Commit graph

8948 commits

Author SHA1 Message Date
俊良
6d8e44666a fix(cli): harden agent view liveness recovery 2026-08-27 01:00:05 +08:00
yiliang114
53dc88564c Merge remote-tracking branch 'origin/main' into agent/agent-view-commands
# Conflicts:
#	packages/cli/src/ui/AppContainer.test.tsx
2026-08-26 20:14:55 +08:00
qqqys
5a883243f4
feat(goal): grant one hand-off turn before a spent budget stops the Goal (#10132)
#9891 stops a Goal the moment its autonomous token budget is spent: the
continuation gate refuses the next turn and settles usage_limited. That
bounds runaway spend, but it cuts the model off mid-thought -- whatever
it had learned in the last window is stranded in the transcript, and
the user who resumes gets no hand-off.

A spent window now buys exactly one more continuation, flagged
`windDown`, whose prompt says the budget is spent, forbids new work,
and asks for a concise hand-off: what was accomplished with evidence
refs, what remains, the one concrete next step. When that turn
finishes, the gate stops the Goal as before. A hand-off turn that finds
the objective already met and proposes completion still completes the
Goal: the stop only ever refuses a continuation, never a verdict.

Exactly one per window, and persisted: the wind-down turn's own
turn_finished record stamps `GoalRecord.windDownTurnId`, so the gate
can tell "hand-off delivered" from "hand-off owed" across a restart
with no extra journal write and no new state cause. A hand-off the
host dropped undelivered leaves no marker and is minted again; a
restart that interrupted the hand-off turn grants it again for the
same reason -- the user never got one. Re-arming the budget on resume
or edit clears the marker, so each window owes its own.

The flag rides the host boundary like verifierFeedback, through all
three hosts, and the prompt block sits after the authoritative
objective line and above verifier feedback; the ordinary prompt is
byte-identical to before.

Mutation probes (goal-runtime + goal-reducer, 218 tests): finishTurn
never stamps the marker -> 3 fail; gate ignores the marker -> 4; gate
never grants -> 6; re-arm keeps the old marker -> 3; parse never
restores it -> 2. Each host hop deleted -> exactly one test fails in
that host's suite (useMessageQueue, useGeminiStream, nonInteractiveCli,
Session).
2026-08-26 11:59:32 +00:00
qqqys
854356d679
feat(goal): let a Goal stop early when its objective is infeasible (#10125)
A Goal whose objective cannot be satisfied as written -- it contradicts
itself, names a target that verifiably does not exist, or needs an
action no tool can perform -- had no sanctioned way to say so. Blocked
proposals stop immediately only for user authority or an external
change; everything else is treated as a repeated technical blocker and
must recur on three consecutive turns before the verifier sees it. So an
impossible objective burned turns until the token budget (#9891) or a
human stopped it. The session that motivated this series ran 34 minutes
on an objective ("验证下版本") too under-specified to ever complete. CC's
stop evaluator can answer `impossible` and end the loop; this is the
counterpart.

`blockerKind: 'infeasible'` joins authority, external and repeated. It
is not a new status: the Goal settles as `blocked`, which every surface
already renders and which resumes into `/goal edit` -- the only fix for
an objective that cannot hold.

Three rules keep it from becoming an "I think this can't be done" exit:

- It bypasses the three-turn repetition rule. Waiting three turns to
  report an impossibility is the runaway this kind exists to end, and
  the evidence bar below is what earns the early exit.
- The cited evidence must include an external_fact. User input can
  authorise a stop (that is `authority`) but cannot make an objective
  impossible, and assistant prose saying so is exactly what must not
  count. Like the other immediate blockers it must also cite every newer
  record, so a contradicting fact cannot be left out.
- The verifier policy accepts it only for self-contradiction, a target
  that verifiably does not exist, or an action outside the tools, and
  rejects difficulty, uncertainty, obtainable information, or a
  preference to ask.

An accepted infeasible stop appends a fixed next step to lastReason, so
the stopped Goal tells the user what to do, not only what went wrong.

Mutation probes (goal-evidence + goal-runtime + goal-tools, 194 tests),
each killing exactly one test: infeasible routed through the repetition
audit; policy sentence removed; external_fact requirement removed; next
step suffix dropped; 'infeasible' removed from the tool schema enum.
2026-08-26 11:59:28 +00:00
易良
ad8e09c663
fix(core): keep allowlist-uncovered tools registered as deferred instead of removing them (#10075) (#10082)
* fix(core): demote permissions.allow-uncovered tools to deferred instead of removing them (#10075)

* test(integration): add E2E regression guard for #10075 uncovered tools

* fix(cli): preserve deferred daemon tools

* fix(core): preserve deferred permission boundaries
2026-08-26 11:58:31 +00:00
qqqys
f9470f570a
feat(core): accept cross-session messages behind an inbound gate (#9576)
* feat(core): accept cross-session messages behind an inbound gate

Step two of QwenLM/qwen-code#8724, rebuilt on current main now that the
registry from step one has landed. A session can be reached by another
session on the same machine, and every message that arrives is gated
before the model can act on it. Off by default behind
`agents.crossSessionMessaging`.

Transport is one UNIX domain socket per session, NDJSON over the wire,
one frame per line. The socket directory is 0700 and the socket 0600,
and that is the whole access-control story: Node cannot read SO_PEERCRED
without a native addon, so a frame's claimed `from` is not
authenticated. Everything downstream assumes that.

The gate is why the transport and the policy land together. With an
explicit `agents.crossSessionInbound` the user decides; unset, the
policy follows approval-mode parity — a message auto-delivers only when
acting on it cannot do more than the sender could already have done
itself. Anything unreadable holds. Held messages are settled rather than
stranded: the buffer is bounded, shutdown expires the rest, and every
terminal outcome goes back to the sender as a control frame.

Content reaches the model inside a <cross_session_message> envelope with
the delimiter defanged in the body, so a peer cannot close the envelope
early and forge one attributed to the user. The envelope carries a fixed
notice that a peer holds none of the user's authority, and the auto-mode
classifier gains the matching rule. `/peers` lists and releases held
messages; without it, holding would be indistinguishable from dropping.

Landed only after four independent reviews, whose non-obvious findings
are worth naming because they were all live defects:

- A receiver in AUTO_EDIT auto-accepted peer messages and nothing
  reviewed what they caused. The old rationale — "every consequential
  action still faces its own gate" — is true of AUTO but false of
  AUTO_EDIT, where edit confirmations are approved outright and the
  classifier does not run. A peer could get a file written with no
  prompt, no classifier and no user. Receiver policy now turns on
  whether the mode reviews actions at all, not on whether it is YOLO.
- `server.unref()` does not cover accepted connections, so any peer that
  connected and lingered pinned the session open forever.
- `fs.mkdir(recursive)` and `fs.chmod` both follow a symlink, so another
  user could pre-create the world-writable `/tmp` fallback directory and
  redirect our 0700 chmod onto a directory of ours.
- A full listen backlog surfaces as EAGAIN on Linux, not EBUSY; the busy
  case was being read as dead on the primary platform.
- The envelope was escapable without markup: `escapeAttribute` handled
  `&<>"` but not newlines, so a crafted `fromName` could emit
  free-standing lines inside the opening tag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(i18n): translate the /peers description in strict-parity locales

`Test (ubuntu-latest, Node 22.x)` has failed every run on this branch with

    FAIL src/i18n/mustTranslateKeys.test.ts
      > does not fall back to English for any built-in command description
        in strict-parity locale { code: 'zh-TW' | 'zh' }
      AssertionError: expected [ 'peers' ] to deeply equal []

`peersCommand` set `description` to a literal English string rather than
routing it through `t()`, so both strict-parity Chinese locales fell back
to English and the parity test — which exists to catch exactly this —
reported the `peers` path.

Four merges of current main were attempted against this check. None could
fix it: the untranslated description is introduced by this branch, so no
state of main contains the missing keys.

Use the `get description() { return t(...) }` form every other built-in
command uses, and add the key to `en.js`, `zh.js` and `zh-TW.js`. All
three locale files remain in sync at 1817 keys.

Verified both directions in a clean worktree install: with the change,
`mustTranslateKeys.test.ts` is 20/20; stashing it reproduces the CI
failure exactly, `expected [ 'peers' ] to deeply equal []`, twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VgTjRF91xANQh6SY9YGyCf

* fix(ipc): close Critical review findings in the cross-session inbox

- Dedup held msgIds case-insensitively, matching /peers resolution
- Print unique dash-stripped handles and resolve them in /peers
- Bound outbound sends: in-flight cap plus an absolute deadline
  (socket.setTimeout is an idle timer a dribbling peer resets)
- Defang whitespace-split and quote-glued envelope delimiter tokens
- Seed onHeldChange subscribers with the current held snapshot
- Skip POSIX-only socket-path tests on win32 like the sibling suites

* fix(cli): gate held-message announcements on the held set growing

Once the hold buffer is full, every further peer frame evicts the oldest
entry while arriving under a fresh id; announcing each of those appends a
history item and re-renders per frame — reintroducing one layer up the
unbounded growth the hold buffer's ceiling exists to prevent.

* fix(ipc): close round-6 Critical findings in the cross-session inbox

* fix(ipc): close round-7 Critical findings in the cross-session inbox

- defang envelope delimiters split by render-invisible separators the
  \s class misses, sharing flattenPeerLabel's strip class so the two
  cannot drift (R5-1)
- restore admission-failed queue submissions deferred until idle, the
  same recovery the /btw path uses, so a restored peer envelope cannot
  leak raw into the racing turn's mid-turn steer drain (R7-1)
- cap the accepted-message backlog symmetric to the held cap: the
  pre-wiring buffer and the post-wiring queue refuse once full, with an
  honest 'expired' receipt instead of unbounded socket-speed growth (R7-11)

* fix(ipc): close round-8 Critical findings in the cross-session inbox

- Refuse any msgId canonicalizing to `all` at the wire boundary and fold
  the /peers bulk keyword case, so a peer-chosen id can never alias the
  bulk action or trap a per-message decision (R8-1, R1-11, R1-27).
- Re-hold approved messages whose delivery fails instead of silently
  dropping them: decide() returns 'failed' and parks the entry back at
  its position, reevaluate() re-holds failed releases, and /peers reports
  the failure honestly (R8-2, R1-18).
- Drain accepted peer envelopes on a preprocessing-free path: the queue
  marks them peer, the drain submits them with the Teammate send type
  (which returns before slash/shell/@ handling), renders the one-line
  projection, and restores failed admissions as peer entries (R8-3).
- Close the envelope-defang class structurally: escape every `<` in peer
  content so no forged delimiter survives regardless of wedged invisibles
  or homoglyph spellings; the open-enumeration regex is deleted (R5-1).

Every guard is witnessed by new focused tests and verified with mutation
probes (each probe fails its witnesses when removed, passes restored).

* fix(ipc): close peer messaging lifecycle races

* chore(cli): regenerate the settings schema for the reworded inbound policy

CI regenerates `settings.schema.json` and fails the Ubuntu job when the
result differs from what is committed. Rewording the `crossSessionInbound`
description in `settingsSchema.ts` (7ca3be72b2, for the AUTO change) moved
the generated file without it being regenerated, so the job never reached
lint, typecheck or tests.

Generated output only — one description string, no behaviour and no
hand-editing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(cli): close peer-inbox admission and decision review gaps (#9576)

* fix(cli): settle all peer-inbox receipts at teardown (#9576)

* fix(cli): restore peer message when its in-flight turn fails delivery (#9576)

* fix(cli): retry restored peer envelope once the failed turn settles (#9576)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: yiliang114 <effortyiliang@gmail.com>
Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
2026-08-26 11:51:31 +00:00
ytahdn
703bcedfab
feat(web-shell): add session token usage panel (#9988)
* feat(web-shell): add session token usage panel

* test: align telemetry mocks with current contracts

* fix(web-shell): address token panel review feedback

* test(serve): align default stats fixture

* fix(web-shell): address token panel review feedback

---------

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
2026-08-26 11:42:26 +00:00
jinye
e5f14e33e0
feat(cli): Add standalone sessions for projectless tasks (#9978)
* feat: Add standalone session containment

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

* feat(cli): Add standalone session service

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

* feat(cli): Adopt standalone sessions for projectless tasks

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

* codex: address PR review feedback (#9978)

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

* fix(cli): defer artifacts on prepare failure, decouple task lookup from runtime lifecycle

- bridgeClient: defer artifact batches when prepareArtifactWorkspace
  rejects instead of dropping them on the floor (R3-2)
- live-task-service: treat quarantined/draining/unavailable
  Conversations runtimes as absent in findStoredTaskRuntimes so
  threads on healthy runtimes still resolve (R3-1)
- Session: drop the local sameManagedConversationPath duplicate in
  favor of the shared isSameConversationPath (R1-22)

* codex: address PR review feedback (#9978)

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

* codex: address PR review feedback (#9978)

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

* codex: address PR review feedback (#9978)

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-26 10:59:16 +00:00
Shaojin Wen
aa7a0f0543
fix(review): give cancelled runs an accurate fallback body instead of the failure comment (#10114)
The fallback-comment gate admits a cancelled review-pr on purpose: a
job-level timeout is auto-CANCELLED by GitHub (failure() false), which
opens neither the failure-only gate nor the in-job step, so silence
there would leave a timed-out review unexplained (#9255). But the same
'cancelled' result also arrives when a run or job is cancelled
mid-review after the upstream chain finished, and that flavor got the
full failure body — "The review pipeline failed before a review could
be posted. A transient error is retried automatically…" — none of
which is true for a cancellation. On PR #9729, run 32875478404 was
run-cancelled two minutes into the review with no successor run, so
the #9716 supersede guard correctly did not match, and the comment
read as a pipeline outage to the PR author.

The two flavors are not separable in needs — both reach the gate as
review-pr 'cancelled' with upstream green — so the fix branches inside
the step on the wired-in needs result: a cancelled review-pr now posts
one body accurate for both flavors (no failure/auto-retry claims,
retry instruction kept for the timeout flavor, run-URL markdown link
kept for the cross-job dedup), and everything else keeps the failure
body. The step runs under set -u, so a dropped env wiring fails the
step loudly instead of silently reverting cancelled runs to the false
body.

Mutation-verified: neutralizing the cancelled branch fails both new
tests; the restored workflow passes the suite at base parity.

Fixes #10109
2026-08-26 10:49:08 +00:00
顾盼
d279dc5634
fix(cua-driver): synchronize release version marker (#10135) 2026-08-26 10:48:58 +00:00
顾盼
f9f5f2fcb6
fix(node-repl): make package verification idempotent (#10133) 2026-08-26 10:06:50 +00:00
顾盼
845bb3f9ad
chore(cua-driver): bump SDK to 0.20.1 (#10127) 2026-08-26 09:52:53 +00:00
zgxkbtl
9b3ccee547
feat(acp): enable managed auto-memory lifecycle (#9992)
* feat(acp): enable managed auto-memory lifecycle

* test(acp): update session fixtures for auto-memory

---------

Co-authored-by: qqqys <qys177@gmail.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-26 09:30:42 +00:00
易良
b22471c4e7
chore(ci): migrate CUA npm packages to trusted publishing (#9836)
* chore(ci): remove stale NPM_TOKEN references from cua-sdk publish

* test(scripts): pin cua sdk trusted publishing workflow

* test(scripts): pin cua sdk publish auth contract

* chore(ci): migrate node repl publish to trusted publishing
2026-08-26 09:29:22 +00:00
顾盼
0d72f767b2
fix(cua-driver): support sandboxed Windows SDK clients (#10120)
* fix(cua-driver): authorize sandboxed Windows SDK clients

* fix(cua-driver): keep Windows secondary actions platform-owned
2026-08-26 09:28:34 +00:00
易良
729b902c9d
fix(core): dedupe hierarchical memory files by canonical identity (#9600)
A workspace-level QWEN.md that symlinks to an ancestor QWEN.md was discovered through two lexical paths, and the Set<string> dedup in discovery only compared path strings. Both aliases were then read and attached to the system prompt (double content, inflated fileCount, duplicated relative @imports). Dedupe discovered candidates by fs.realpath identity, keeping the first-discovered lexical path so ordering, display, and import resolution are unchanged. Fixes #9597
2026-08-26 09:20:35 +00:00
dreamWB
e086614086
fix(web-shell): constrain responsive sidebar drawer (#10020)
* fix(web-shell): constrain responsive sidebar drawer

* fix(web-shell): disable resizing in sidebar drawer

* test(web-shell): clarify sidebar drawer invariants

---------

Co-authored-by: dreamWB <dreamWB@users.noreply.github.com>
2026-08-26 09:15:58 +00:00
qqqys
6cab1f7f8b
feat(goal): stop autonomous continuation at a token budget the user re-arms (#9891)
* feat(goal): stop autonomous continuation at a token budget the user re-arms

A Goal run in this repository has no autonomous termination path: every
stop so far is either the model completing, a specific enumerated bound,
or a human typing /goal pause. The two runaway sessions that motivated
this series both ended the third way -- one after 8.6M tokens in 34
minutes. Precision fixes remove the loop families we have found; this
adds the bound that covers the families we have not.

Every newly created Goal is armed with an autonomous spend window
(GOAL_DEFAULT_TOKEN_BUDGET, 30M tokens on the `tokensUsed` metric the
recorder already bills per turn). The gate sits in `queueContinuation`,
the single point every autonomous continuation is minted through, so one
check bounds turn cadence, verifier-rejection retries, checkpoint cycles,
and loops not yet discovered. When the window is spent the runtime
settles the Goal as `usage_limited` with the new `limitKind:
'token_budget'` instead of minting the continuation. User-driven turns
never pass through the gate and are never blocked.

The budget is an authorization quantum, not a fault: resuming a
budget-stopped Goal moves the ceiling to `tokensUsed + grant` -- the
meter itself is never reset -- and the same re-arm applies to an edit of
a spent Goal, so both explicit user actions buy another window. An
unattended runaway stops and stays stopped, because nobody is there to
resume it. Goals persisted before budgets existed restore unbounded, and
a host can opt out with a non-finite grant, which arms nothing rather
than persisting a value the JSON journal cannot carry.

The reducer's evidence-limited resume refusal now matches the two
evidence kinds instead of any `limitKind`, so a budget-stopped Goal is
not misread as evidence-limited. The SDK union and the webui mapper
whitelist carry the new kind across the wire, and the unpermitted
get_goal summary reports `tokenBudget` beside the `tokensUsed` it
already exposed.

Mutation probes: deleting the continuation gate fails exactly the
budget-stop test (110 others green); forcing the re-arm helper to return
nothing fails exactly the three re-arm tests (187 others green).

* fix(goal): re-arm spent budgets consistently

* fix(goal): unify spent-budget checks and settle on failed writes

* test(core): pin evidence window retention on budget-stopped Goal resume (#9891)

* docs(core): spell out the Goal budget meter's coverage (#9891)

The window bills Goal-turn model calls only; per-turn side queries and
checkpoint-verifier calls are unmetered, so real provider spend at a
stop runs above the ceiling. Also revert an unrelated one-line reflow
in uiTelemetry.test.ts flagged as churn in review.

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: qqqys <qys-us2@outlook.com>
2026-08-26 09:15:21 +00:00
qqqys
647fdff036
feat(providers): load model recommendations before editing (#9980)
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
Security Checks / Dependency CVE audit (push) Waiting to run
Security Checks / Secret scan (TruffleHog) (push) Waiting to run
* feat(providers): load model recommendations before editing

* fix(providers): harden model discovery after review round 1 (#9980)

* fix(providers): stop curating discovered model ids (#9980)

* fix(providers): reject full unsafe unicode classes in discovered model ids (#9980)

* fix(providers): close the unassigned-code-point gap in model id validation (#9980)

* test(providers): pin the lone-surrogate model id rejection (#9980)

* test(cli): step past Grok in the MiniMax endpoint auth walk (#9980)

The stdin-driven walk pressed down once from DeepSeek expecting MiniMax,
but the Grok (xAI) preset landed between them on main. The suite skips
under CI=true, so only local runs saw the stale adjacency.

* fix(cli): clear stale model-ids error and lead submits with served models (#9980)

Two Critical review findings on the discovery wizard. On the discovery
path edits never cleared the empty-submit error banner because they only
call the no-op sync; add a clearModelIdsError flow action for that
branch. And mergeModelIds put free-form ids first, so a partial-catalog
no-edit submit led with an unserved demoted default, which
buildInstallPlan writes as model.name on first-time setup; checked
recommendations lead now.

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
2026-08-26 09:02:28 +00:00
易良
fc874dfe0b
fix(ci): retry sandbox image builds and file an issue when a release build fails (#9916)
* fix(ci): retry sandbox image builds and file an issue when a release build fails

The v0.22.0 tag build died on a transient ETXTBSY during `npm ci` and was
never retried, so ghcr.io/qwenlm/qwen-code:0.22.0 was never published while
npm already served 0.22.0. Every sandbox-based CI lane (/resolve, sandboxed
review, autofix) then crashes with "manifest unknown" until the image exists.

Add one bounded retry to the buildx step: the first attempt carries
continue-on-error so a successful retry turns the job green, and the retry
gates on the first attempt's outcome alone (a failure() gate would read false
once continue-on-error absorbs the first attempt). The publish condition is
shared through one job-level env so the two build steps cannot drift.

Add a follow-up job that files or updates one issue per version when both
attempts fail — for tag pushes and for publishing dispatches alike, since the
issue body itself recommends that dispatch as the recovery path. Dedup uses
an exact body marker matched client-side, because GitHub search tokenizes the
colon out of the marker and never finds these issues.

Extend the existing workflow gate test to pin the retry contract and the
issue-job gate.

Fixes #9898

* fix(ci): move the image-build failure issue logic to .github/scripts/

The workflow-size ratchet rejects growth past the recorded baseline +4096
bytes; the inline issue-filing step grew build-and-publish-image.yml by
~5.2 KB. Move the step body to .github/scripts/image-build-failure-issue.sh
(the gate's own recommended remedy), leaving the job as a thin env + script
call. No behavior change; the gate test now pins the script call and reads
the dedup contract from the script.

* fix(ci): grant the failure-issue job contents permission and normalize dispatch versions

* test(ci): pin the failure-issue gate and retry step invariants

* fix(ci): gate the failure-issue job on the exported publish decision

* fix(ci): skip the failure-issue job for versionless publishing dispatches

* test(ci): pin the PUSH_IMAGE value and the login gate at the definition site

* test(ci): replay the image-build failure-issue script under a gh stub

* fix(ci): describe release build job failures without asserting a buildx cause

* fix(ci): preserve annotations and recorded runs when updating the failure issue

* test(ci): pin the dedup label on create and the open-state filter on lookup

* fix(ci): document the pre-first-step gap in the failure-issue gate

A build job that fails before its first step runs (runner provisioning
failure) never executes publish-decision, so push_image stays empty and
file-failure-issue is skipped despite failure() being true. Closing the
gap structurally would restate the publish predicate and re-introduce
the drift this PR removes, so document it on the job comment instead:
future "failed publish, no issue filed" investigations start here, and
a scheduled npm-vs-GHCR reconciliation remains the backstop.

* fix(ci): record build-and-publish-image.yml's shipped size in the workflow size baseline

The retry logic and failure-issue filing steps added by this PR grew the
workflow from 4638 to 8887 bytes, past the 4096-byte allowance. Record
the new size so the size gate passes.

* fix(ci): harden the image-build failure reporter per review round 4 (#9916)

- Replace GNU-only `head -n -1` with POSIX `sed '$d'` so the stranded-heading
  strip no longer corrupts the body on BSD userland (R4-1).
- Skip the bash replay suite on win32, where backslash RUNNER_TEMP and the
  ';'-separated PATH cannot express it; the YAML pins still run there (R4-2).
- Re-check head readability AFTER the normalization strip, which can itself
  empty the head and used to drop the narrative permanently (R4-8).
- Admit only recorded-run shapes into the machine block so a bullet-shaped
  human annotation is no longer reordered into it or clipped by the cap (R4-13).
- Remove the marker-restore branch: with the run shape pinned, every body that
  matched the dedup carries its marker in head+tail, so it was unreachable (R4-9).
- Cross-reference the sibling split/merge contract in both implementations (R4-5),
  disable SC2016 with rationale on the literal-backtick formats (R4-6), and
  document the label-removal residual gap on the job (R4-11).
- Behavioral witnesses: run-cap, stranded-heading, marker-survival, empty-head
  and empty-after-strip prose fallbacks, and the annotation shape; each guard
  mutation-probed red. Pin the dedup label on the list call too (R4-10).

* fix(ci): document the version-marker dedup gap on the failure-issue job (#9916)

Round 4 removed the unreachable marker-restore branch (R4-9) but left its
residual gap undocumented: the dedup lookup only finds the tracked issue
while the version marker survives in the body, so a human edit deleting
the marker orphans the issue and the next failure files a duplicate. Fold
the marker into the job's existing known-gap note alongside its sibling,
the scope/ci-cd label (R4-11), and record the workflow's new size.
2026-08-26 08:52:56 +00:00
Yu Zhang
652cd76781
docs: consolidate assorted documentation corrections (#9855)
* docs(zed): use real debug log env var (#9853)

* docs(headless): fix JSON jq examples (#9858)

* docs(cli): align status line default preset prose (#9864)

* docs(acp): update permission mediator status (#9866)

* docs(sdk): align query option reference (#9855)

---------

Co-authored-by: zhangyu.34 <zhangyu.34@bytedance.com>
Co-authored-by: 易良 <1204183885@qq.com>
2026-08-26 08:52:13 +00:00
qqqys
ba657c0678
feat(core): trust a generated-scripts root for workflow scriptPath loads (#9987)
* feat(core): trust a generated-scripts root for workflow scriptPath loads

`Workflow({scriptPath})` and `workflow({scriptPath})` only load files that
resolve inside the two saved-workflow directories, and every file in those
directories is also a `/<name>` slash command. A tool that generates a
workflow script for a single run therefore had no place to put it: writing
into `.qwen/workflows` hands the user a permanent command for a run that is
already over, and any other path is refused by the loader.

Add `<projectDir>/workflows/generated` as a third trusted root for
`{scriptPath}` loads only. It lives in the runtime dir beside the run
snapshots and journals, is never enumerated by `listSavedWorkflows`, and
cannot be reached by `workflow('<name>')`. The same realpath boundary check
and symlinked-root refusal apply. The tool description stops claiming that
`scriptPath` accepts a path anywhere.

Part of #8769.

Claude-Session: https://claude.ai/code/session_017cUwuTey4APA8wAyAM6ScS

* fix(core): disambiguate the generated-workflow root and name refusal roots (#9987)

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

* fix(core): label generated workflow scripts and name refused symlinked roots (#9987)

* fix(core): classify workflow scriptPath labels by normalized path (#9987)

* fix(core): classify workflow confirmation labels by canonical path (#9987)

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qqqys <qys-us2@outlook.com>
2026-08-26 08:46:02 +00:00
jinye
7b69293266
feat(daemon): Support current-session scheduled tasks (#9838)
* docs(scheduled-tasks): design current-session creation entrypoints

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

* docs: clarify scheduled task session semantics

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

* feat(daemon): Support current-session scheduled tasks

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

* codex: fix CI failure on PR #9838

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

* codex: fix CI failure on PR #9838

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

* codex: fix CI failure on PR #9838

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

* codex: address PR review feedback (#9838)

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

* codex: fix CI failure on PR #9838

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

* codex: address PR review feedback (#9838)

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

* codex: address PR review feedback (#9838)

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-26 07:58:52 +00:00
qqqys
44762ef40c
refactor(cli): remove unused useInputHistoryStore hook (#10041) 2026-08-26 07:38:30 +00:00
pratik wayase
c7f7b2d975
fix(ui): suppress duplicate identical TodoList panels in a single turn (#9692)
* fix: deduplicate identical TodoList panels in single turn

* fix(ui): address review - type unchanged, fix reminders, fix snapshot

* fix(ui): address review comments - extract reminder helper, fix TS types, and add no-op test coverage

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-26 07:30:14 +00:00
Yu Zhang
53b0e4b57b
fix(vscode): sync companion token limits (#9850)
Keep the browser-safe companion token limit mirror aligned with core for newer DeepSeek, GLM, and MiniMax models.

Co-authored-by: zhangyu.34 <zhangyu.34@bytedance.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-26 07:21:47 +00:00
qwen-code-dev-bot
38c5f9b4fd
fix(ci): yield the event loop between script tests to avoid vitest RPC timeouts (#10037) (#10050)
* fix(ci): yield the event loop between script tests to avoid vitest RPC timeouts (#10037)

The v0.22.1 release quality job exited 1 on `npm run test:scripts` with
every test green. vitest's worker->main `onTaskUpdate` RPC has a fixed 60s
timeout; the synchronous spawnSync-driven script suites keep a forked
worker's event loop blocked for an entire file (~66s on the heaviest
suite), so the queued RPC response is never processed before the timer
fires, surfacing as an unhandled `[vitest-worker]: Timeout calling
"onTaskUpdate"` error. Linux keeps unhandled errors fatal (the scripts
vitest config only exempts non-Linux since #9728), so the release died.

Add a global per-test event-loop yield to the scripts test setup. The
timer is captured at setup load so `vi.useFakeTimers()` inside a test
cannot intercept the yield. Any continuous stall is now bounded by a
single test, so RPC responses drain long before the 60s deadline. Real
test failures stay fatal on every platform; the Linux unhandled-error
signal is untouched.

* fix(ci): state the actual yield invariant in the script test setup comment (#10037)

* test(ci): pin the script-test event-loop yield invariant (#10037)

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-26 07:11:15 +00:00
Yan Shen
e38665674e
fix(core): preserve images for multimodal DeepSeek (#9854) 2026-08-26 06:50:36 +00:00
俊良
2435de5901 fix(cli): acquire swap latch before resume guard 2026-08-26 14:20:31 +08:00
Yu Zhang
06600b364c
refactor(core): remove unused LruCache utility (#9926)
Drop the unreferenced LruCache helper and its isolated tests so the legacy filename allowlist no longer tracks dead core utility code.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

Co-authored-by: zhangyu.34 <zhangyu.34@bytedance.com>
2026-08-26 06:16:03 +00:00
俊良
28aaff2847 Merge remote-tracking branch 'origin/main' into codex/pr-7802-main-clean 2026-08-26 13:27:33 +08:00
Zqc
d4664fdc89
fix(core): sync loaded-skill state with history eviction (#9500)
* fix(core): sync loaded-skill state with history eviction

Refs #6762

Split from PR #8900 per maintainer direction review: this keeps only
the eviction-state sync half (loaded-skills tracking reconciled with
history evictions across microcompaction, /compress-fast, memory
pressure compaction, client retry, and ACP settle). The /unskill
command is deferred to a separate change pending the #6762 design
discussion.

* fix(core): key skill-hook dedup on the whole prepared config

The dedup identity keyed only on type + command/url, but the
frontmatter admits multiple hooks per matcher distinguished solely by
fields that key ignores (timeout/shell for command hooks,
headers/timeout for HTTP hooks): the second of such a pair was
silently skipped even on the skill's first registration. Key on the
whole prepared config instead — frontmatter configs carry no
functions, so the structural key stays stable across reload cycles.

Adds regression tests for both shapes.

* fix(core): address round-2 review — setHistory reconcile, strip-occurrence gate

* fix(core): gate skill-body residency on SkillTool provenance

- Record exact body outputs in SkillTool as residency provenance; all
  residency checks fail closed against a fresh process (empty set)
  instead of trusting the two public markers (spoofable via the shared
  functionResponse{name:'skill'} shape)
- Make setHistory's reconcile the single loaded-skill sync: remove the
  five post-setHistory second writes (3x syncSkillEvictions, hard-rescue
  reconcile, tryCompress blanket clear) and delete the now-dead helpers
- Correct the settle-reconcile comment to name its five pre-try skip
  paths; resume stays intentionally un-enumerated (fail-closed reconcile
  plus one bounded duplicate body beats a marker-based resume door that
  would reopen the injection window)
- Tests: provenance gating (injection/genuine/spoof-stripped), stateful
  Set oracle for clear-before-track order, microcompact F2 witness and
  size-path kept-body twin, mixed-batch unload, isRetry settle arming,
  wire-after-setup hard-rescue, reconcile-only tryCompress, and the
  three second-write sites flipped to single-writer assertions

* fix(core): classify stripped skill responses by call id (R4-1)

The scheduler's persistence gate rewrites large genuine skill bodies
into <persisted-output> stubs before they enter history. The strip
path's provenance/shape check skipped such stubs, leaving the skill
tracked with no resident body — the #6762 deadlock. unloadSkillsFromEntries
now classifies every stripped skill response by call-id resolution:
fail-open direction, since over-un-tracking self-heals with one
duplicate body while under-un-tracking deadlocks reload; the
resident-sibling filter keeps precision.

Also pins the R4 findings: provenance recording both halves through
the real SkillTool, microcompact provenance wiring at the client call
site, the ambiguous call-id policy on both strip and reconcile sides,
provenance-mode targeted unload, compressFast's setHistory reconcile,
and the forked-chat compression/restore observable effects. Rewrites
the evictedSkillNames docstrings to the diagnostic-only contract.

* test(cli): complete stale fakes in session-swap telemetry test

#9764 added getCurrentCustomTitle/getSessionDisplayName calls to
useBranchCommand after #9844's test fakes were written, so the suite
throws before forkSession on any branch carrying both. Fill the two
missing fake methods; main CI has not run since before either landed.

* refactor(core): reduce skill-eviction sync to conservative clear at rewrite boundaries

Per #9500 review: drop the process-wide body provenance set and history
scans, the evictedSkillNames/unresolvedEvictedSkills diagnostics, the
exact-reconcile vs targeted-unload algorithms, and the ACP settle
machinery. Instead, conservatively clear loaded-skill tracking at the
three destructive history-rewrite boundaries (setHistory covers every
compaction path, truncateHistory, stripOrphanedUserEntriesFromHistory),
guarded so forked chats never touch the parent's tracker. Over-clearing
self-heals with at most one duplicate body on the next invoke; a stale
entry made the skill permanently unreloadable.

Kept: authoritative-vs-forked ownership guard, idempotent hook and
allowedTools re-registration on reload, and a small behavioral test set.
Exact lifecycle semantics, explicit unload, and richer eviction
diagnostics belong under #6762 in follow-up PRs.

---------

Co-authored-by: 俊良 <zzj542558@alibaba-inc.com>
Co-authored-by: yiliang114 <yiliang.yyl@alibaba-inc.com>
Co-authored-by: 易良 <1204183885@qq.com>
2026-08-26 04:46:57 +00:00
易良
a770aefddd
fix(mcp): recover restarted HTTP MCP servers in-session and in CLI (#9962)
* fix(mcp): recover restarted HTTP MCP servers in-session and in CLI

An HTTP MCP server that restarts comes back with a fresh mcp-session-id
space. Four defects then stacked up to keep its tools unusable until a
full Qwen Code restart (issue #9944):

* The in-session auto-reconnect only fired for tools carrying
  readOnlyHint/idempotentHint annotations: `handleReconnectOnError`
  treated "not safe to auto-replay this call" as "do not reconnect at
  all", so unannotated tools never refreshed the session. Reconnect
  (fresh initialize + tool reload) is safe regardless of replay safety —
  only the replay of the ambiguous call is not. Repair now always runs;
  the ambiguous call still fails without being replayed.
* The restart's canonical "-32001 Session not found" failure was not
  recognized as a connection error, and while the client status was
  still CONNECTED it was misclassified as an execution timeout, never
  reaching the reconnect path. Both gaps closed.
* `McpClient.disconnect()` abandoned the server-side session: the SDK's
  transport.close() only tears down local state. Per spec the client
  SHOULD terminate the session via DELETE, and without it single-session
  servers reject every later initialize with "Server already
  initialized" (permanent breakage), while multi-session servers
  accumulate orphaned sessions. terminateSession() now runs on
  disconnect, best-effort.
* `qwen mcp reconnect` lied and hung: it reported success off a
  best-effort discovery that swallows connect errors (now verified via
  the server status), claimed nothing about its process scope (now
  noted), and never exited because background incremental discovery
  re-armed a ref'd health-check timer after shutdown (skip the
  background pass; unref the timer).

* fix(cli): exit non-zero when mcp reconnect --all has failures

Per-server errors are caught inside reconnectAllMcpServers and never
rethrown, so the handler's process.exit(exitCode) never ran in --all
mode: 'qwen mcp reconnect --all' exited 0 even when some or all
servers failed verification, while the single-server path exits 1 for
the identical failure. Wrapper scripts running
'qwen mcp reconnect --all || alert' would never alert.

Track failures in the loop and throw the same ReconnectError after the
summary/scope note (the finally shutdown still runs), and assert the
exit code in the --all tests.

* fix(cli): report skipped mcp servers distinctly in reconnect

A disabled server, a pending-approval .mcp.json server, or an untrusted
workspace makes discovery return early — no connection attempt, no
status write — and getMCPServerStatus defaults never-seen servers to
DISCONNECTED, so every skip surfaced as 'connection attempt finished
without a live connection', sending whoever is debugging to chase
networking for a server the client never tried to contact.

Check the cheap, knowable skip causes before declaring a connection
failure and report the real reason instead.

* fix(mcp): bound session termination in disconnect() teardown

disconnect() awaits transport.terminateSession() — a network DELETE
with no timeout anywhere in the chain: the SDK call has none, its only
cancellation is the transport's abort controller which close() aborts
only after this await, and the MCP undici dispatcher runs with
headersTimeout: 0, bodyTimeout: 0. A live-but-unresponsive server
(TCP open, never answers the DELETE) hangs disconnect() indefinitely,
and every await client.disconnect() caller with it — manager stop,
Config.shutdown, health-check reconnect, and the in-session repair
path. Pre-fix disconnect() was purely local, so this hang class was
newly introduced by the terminateSession call.

Race the DELETE against a short unref'd timeout inside the existing
try/catch; close() runs immediately afterwards and aborts the
still-in-flight request. Regression test covers a never-responding
terminateSession.

* fix(core): route terminated/expired session errors to reconnect path

The reconnect pattern matches three dead-session phrasings
(not found | terminated | expired), but the sibling -32001 timeout
carve-out matched only 'session not found', so the other two variants
were misrouted to EXECUTION_TIMEOUT instead of the reconnect path
whenever the client-side status had not flipped to DISCONNECTED yet.

Widen the carve-out to the same three variants and extend the
CONNECTED-status reconnect test to an it.each over all three, so a
future regression narrowing either regex can no longer hide behind a
green suite that only exercises 'Session not found'.

* fix(cli): shut down reconnect config on single-server failure

The single-server path only ran config.shutdown() after a successful
verification; a failed connection attempt threw, the handler's
process.exit(1) killed the process, and any stdio MCP server the attempt
spawned was orphaned (process.exit does not terminate spawned children).
Mirror the --all try/finally structure so shutdown runs on both paths.

* test(cli): assert session-scope note prints on --all failure

Both --all failure tests only covered the per-server lines, the summary,
and the exit code. The note is written before the failure throw today,
but an edit moving it below the throw would silently drop the
restart-your-session guidance from failure output while the suite stays
green. Lock the failure-path output down.

* fix(cli): exclude deliberately skipped mcp servers from --all failure count

--all counted servers discovery intentionally skips (disabled,
pending-approval .mcp.json) as failures, so one skipped server forced
exit code 1 on every run — a wrapper running
`qwen mcp reconnect --all || alert` would alert forever while nothing
was attempted-and-failed. Throw a typed SkippedConnectionError from the
verification step, report skips as an informational line, and reserve
the non-zero exit for servers that were actually attempted and failed.

* fix(cli): pass workspace trust state to mcp reconnect config

createMinimalConfig built the throwaway Config without trustedFolder, so
isTrustedFolder() always defaulted to true: the untrusted-skip reporting
branch was unreachable in production, and the command would attempt
connections a normal session's discovery gate skips. Wire the real
workspace trust state through so both gates agree.

* fix(cli): wire allowed/excluded MCP gates into the reconnect config (#9944)

* refactor(core): single shared dead-session pattern for both reconnect decision sites

The dead-session matcher was hand-duplicated across MCP_CONNECTION_ERROR_PATTERNS
(drives shouldAttemptReconnect) and the isExecutionTimeoutFailure carve-out, with
the keep-in-sync invariant enforced only by a comment. Extract
MCP_DEAD_SESSION_ERROR_PATTERN and consume it from both sites so the reconnect
matcher and the timeout carve-out can never drift apart (a divergence misroutes a
covered variant either into a hard EXECUTION_TIMEOUT or past the reconnect
matcher). Behavior unchanged; pinned by the existing per-variant it.each.

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

* refactor(core): reuse runWithTimeout for the bounded terminateSession wait

disconnect()'s bounded DELETE hand-rolled a third timer-race implementation
(setTimeout + Promise.race + manual clearTimeout) for the same shape
runWithTimeout from mcp-discovery-timeout.ts already provides to the pool
spawn/restart bounds. Reuse it at this new site; on timeout its rejection
lands in the existing catch (debug-logged) and teardown still falls through
to close(), so the no-hang contract is unchanged (pinned by the existing
"disconnect() does not hang when terminateSession never responds" test).

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

* fix(mcp): surface the connect failure cause in qwen mcp reconnect output

The reconnect command's failure message carried no underlying cause and the
cause was not retrievable anywhere in its process: McpClient.connect/discover
throw into the manager's best-effort discovery catch, which logs via
debugLogger only (createMinimalConfig hardcodes debugMode:false), and the
status registry stored only the enum — leaving "connection attempt finished
without a live connection (status: disconnected)" with no direction for
debugging.

Extend the existing status registry with a per-server last-error carrier
(recordMCPServerLastError / getMCPServerLastError, auto-cleared on CONNECTED
or registry removal), record the cause in McpClient.connect()/discover()'s
failure catches before the manager swallows them, and append it to the
reconnect command's verification failure — single-server and --all alike.

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

* fix(cli): print the session-scope note on the single-server reconnect failure path

SESSION_SCOPE_NOTE was the one output path this diff left unpinned on
failure: the single-server path printed it only on success, while --all
prints it even when servers fail (with a pinning test whose rationale binds
both paths). A failed reconnect is exactly when the user's running session
still has broken tools, so print the note on the single-server failure path
too and pin it with a matching test.

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

* fix(mcp): report allow-list and excluded servers as skipped in qwen mcp reconnect

The throwaway reconnect Config wires the mcp.allowed / mcp.excluded gates,
but describeSkippedConnectionReason had no branch for them: an
allow-list-blocked server fell through every classifier branch and was
misreported as a failed connection (status: disconnected) on every run,
forcing exit 1 forever under --all, and an mcp.excluded server was
misattributed to a per-server disabled flag that does not exist. Reuse
Config.getMcpServerUnavailableReason so both gates report as deliberate
skips that do not count toward the failure total (issue #9944).

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

* fix(mcp): treat HTTP 404 as a dead-session signal for in-session recovery

Dead-session recovery was decided by a hand-listed set of English
phrasings matched against server error prose, while the spec-pinned
structural signal — HTTP 404 on the POST carrying a stale mcp-session-id,
surfaced by the SDK as code: 404 on StreamableHTTPError — was never
consulted. A restarted server answering 404 with non-enumerated prose
(e.g. "Unknown session") never triggered repair, so every subsequent
call re-POSTed the stale session id and failed. Treat code 404 as a
dead-session signal in shouldAttemptReconnect alongside the existing
prose patterns (issue #9944).

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

* test(mcp): pin the discovery failure cause carrier in discoverAndReturn

The failure-cause recording discoverAndReturn()'s catch adds had no
test — deleting the recordMCPServerLastError line left the suite green.
A server whose connect() succeeds but discovery fails (up-but-empty)
reaches this catch, and qwen mcp reconnect relies on
getMCPServerLastError to print the cause. Drive connect-ok/discovery-fail
and assert the carrier holds it (issue #9944).

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

* fix(mcp): keep the runWithTimeout rejection message accurate for disconnect()

The hard-coded tail — "pool will roll back the spawn/restart and free
its budget slot." — described pool semantics that do not apply to
McpClient.disconnect()'s terminateSession call site, which never
touches the pool: on timeout its catch logs and falls through to
transport.close(). Anyone debugging a hung teardown was pointed at
machinery (pool spawn/restart rollback, budget slot) that path never
exercises. Make the helper's rejection message generic and fold the
rollback clause into the three pool call sites' labels, where it is
accurate (pool spawn + unpooled spawn in mcp-transport-pool.ts, pool
restart in mcp-pool-entry.ts) (issue #9944).

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

* fix(mcp): gate lastError recording on isDisconnecting like the status write

connect()'s and discoverAndReturn()'s catch blocks recorded the failure
cause unconditionally while the adjacent updateStatus(DISCONNECTED) write
is suppressed once disconnect has begun. A server disabled/removed
mid-connect therefore had its status entry dropped by removeMCPServerStatus
but the doomed in-flight connect's late rejection resurrected an orphan
lastError entry persisting until process exit, contradicting mcp-status.ts's
documented drop-on-removal invariant (and misattributing to a later
re-added incarnation). Gate both record sites on !isDisconnecting,
mirroring updateStatus; extend the stale-connect regression test to pin
getMCPServerLastError staying clean post-removal.

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-26 04:34:09 +00:00
Shaojin Wen
0756be0ce7
ci: take the macOS and Windows lanes off pull requests (#10059)
* ci: take the macOS and Windows lanes off pull requests

Both lanes were costing contributors more than they were telling them.
Measured over the 18 hours before this change, on pull requests:

  - Windows reported 13 failures and 0 successes. The failures are one
    standing set of Windows-only path and symlink cases — resolved paths
    into read_many_files, releaseWorktree through an ancestor symlink,
    the SHA-256 review worktree — repeating across unrelated PRs, so the
    red X almost never belonged to the diff under it.
  - macOS queued for a p90 of 42 minutes and up to 159, on a hosted pool
    this repository does not saturate by itself: 20 of the 63 sampled
    waits happened with zero macOS jobs of ours running.

Neither lane gates a merge — the `main` ruleset carries no required
status check — so none of that waiting or noise was buying protection.
Leave them on the nightly, the merge queue and dispatch, and drop the
pull-request arm plus the classifier job that existed only to feed it.

That makes the nightly load-bearing rather than a backstop: it is now
the only report of a non-Linux regression, so add a guard for the three
ways it could go quiet — the schedule disappearing, a lane no longer
accepting it, or a third job joining it and failing the run for reasons
that have nothing to do with either platform.

The classifier, its script mode and both test files are left in place so
restoring the pull-request trigger, once the Windows failures are fixed,
is a revert plus two `if` arms.

* ci: pin the retired-classifier contract in ci-platform-lanes.test.js

The lane change left scripts/tests/ci-platform-lanes.test.js pinning the
shape it removed — a sensitive-PR trigger arm and a live classify_platform
job — which failed the Test job on this branch. Rewrite the suite to pin
the new contract instead: both lanes run on the schedule, the queue and
dispatch and on nothing else; the pull-request arm and the classifier are
gone whole (a half-restoration — a trigger without its classifier, or the
reverse — fails); the classifier's own scripts stay tested so restoring
the trigger stays a clean revert; the nightly still reaches exactly the
two lanes and its failure still files an issue.

That suite already owned the nightly-liveness assertions, so the
platform-lane-triggers.test.mjs guard added earlier on this branch
duplicated it — dropped, along with its HELPER_TESTS entry.

Verified by mutation: deleting the schedule, dropping schedule from one
lane, letting a third job onto the nightly, and reintroducing the
pull-request arm without its classifier each fail the suite; the branch
shape passes 17/17.
2026-08-26 03:31:55 +00:00
ytahdn
31ad20befe
fix(cli): preserve bridge timeouts for mid-turn media (#9995)
* fix(cli): preserve bridge timeouts for mid-turn media

* fix(cli): separate mid-turn bridge timeouts

---------

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
2026-08-26 01:57:01 +00:00
ytahdn
48645c56f8
feat(web-shell): make compact view the only mode (#9993)
* feat(web-shell): make compact view the only mode

Remove the ui.compactMode toggle (Ctrl+O shortcut, settings persistence,
help entry, i18n copy) and fix the compact rendering on for every message
surface via a single root CompactModeContext provider — main chat, split
panes, subagent detail panel and the drawer variant. The daemon-side
setting registration stays untouched; the web shell keeps it hidden from
the settings panel.

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

* fix(web-shell): address compact-mode retirement review feedback (#9993)

- Mark ui.compactMode retired everywhere in the settings docs and schema
  description (regenerated), matching the always-on compact view; mark the
  long-gone ui.compactInline row as removed.
- Keep Ctrl+O suppressed globally after the toggle removal so the key never
  falls through to the browser's Open File dialog, with a pinning unit test.
- Add discriminating coverage: a settings-panel test that fails if
  ui.compactMode leaves HIDDEN_SETTING_KEYS, and an e2e spec asserting the
  merged compact summary row so flipping the app-level provider fails.

* fix(web-shell): address round-2 compact-mode retirement review (#9993)

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

* fix(web-shell): revert out-of-footprint test-config guard (#9993)

The verification gate rejected the round-2 commit because it added
dangerouslyIgnoreUnhandledErrors to packages/web-shell/vitest.config.ts,
a test-config file this PR never legitimately touched. Review feedback
cannot authorize changes to CI/verification machinery, so revert the
file to main. The failing Windows/macOS test lanes the guard targeted
are escalated to a maintainer as an open question instead.

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

---------

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-26 01:43:40 +00:00
易良
a6d30ebc6b
fix(core): register report_findings in the permission alias table (#10033)
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 / 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
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
Security Checks / Dependency CVE audit (push) Waiting to run
Security Checks / Secret scan (TruffleHog) (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
2026-08-25 15:50:21 +00:00
易良
2ef511a07f
fix(cli): drop duplicate object properties breaking the main build (#10022)
#9998 re-added getSessionDisplayName and getCurrentCustomTitle to the
session-swap telemetry fakes, which already declared both. TS1117 fails
tsc --build, breaking every workflow that runs npm run build on main.
2026-08-25 14:08:59 +00:00
qqqys
463809cbb3
feat(goal): stop a Goal whose checkpoints stall three times in a row (#9975)
* feat(goal): stop a Goal whose checkpoints stall three times in a row

#9835 made a truncated evidence window compact instead of stopping the
Goal. That removed the only terminator on the path where compaction
runs and gives no relief: a Goal whose evidence rate outruns the
catalog pays a checkpoint verifier call every turn, loses evidence every
turn, and never converges on its own.

A checkpoint is counted as stalled when it comes back holding the
maximum number of claims while the window it compacted was already
truncated. Compaction has two levers, folding evidence into claims and
moving the cursor past what was folded; that combination means the
first is exhausted (the next checkpoint can only merge) and the second
is not keeping up (eligible evidence was left behind). A busy turn that
truncates with room in the claims is not a stall, and a full claim list
on a quiet Goal is not either.

The streak is persisted as `GoalRecord.checkpointStalls` (absent means
zero) so a restart or resume cannot launder it. Any check that finds
room resets it, and so do edit and replace. After three consecutive
stalls the Goal settles as `usage_limited` with the existing
`limitKind: 'evidence_catalog'` and a reason naming what stalled, how
many times, and what to do. No new limit kind, no branch in
`goalLimitKindForReason`, nothing crosses the wire.

Mutation probes (goal-runtime + goal-reducer + goal-checkpoint, 192
tests): no increment -> 3 fail; no reset on an effective checkpoint ->
1; no reset on a quiet check -> 1; threshold >= to > -> 1; parse drops
the key -> 1; parse never restores it -> 1; predicate ignores
truncation -> 2; predicate ignores the claim cap -> 1; edit stops
resetting -> 1. Every other test green in every run.

* fix(goal): preserve the stall streak when a check proves no room (#9975)

finishCheckpointCheck reset checkpointStalls on all three call sites, but
only the room branch proved the window had relief. A transient verifier
failure or an empty turn now preserves the streak, so intermittent
checkpoint-verifier errors cannot launder the count and keep the stall
breaker from firing. Also moves withCheckpointStalls out from between
takeTurnTokens and its JSDoc.

* fix(goal): count unusable checkpoint results toward the stall limit (#9975)

* fix(goal): surface unusable checkpoint verifier output to the stall breaker (#9975)

* fix(goal): reset the stall streak when a resume restarts the evidence window

#9840 landed after this branch opened: an evidence-limited Goal now
resumes by repointing the cursor and dropping the checkpoint, which is
a different evidence window from the one the streak was counted
against. Carrying the count across it spends the new window's
allowance on the old window's failures -- a Goal resumed at two stalls
would stop again after a single stalled checkpoint.

A resume that does NOT restart the window (paused, blocked) keeps the
streak: that Goal comes back to the same window, so what it learned
about that window is still true.

Mutation probe: removing the reset fails exactly the new resume test
(75 others green).

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-25 14:08:50 +00:00
qqqys
526809dd91
fix(serve): let channel workers reach TLS-enabled daemons (#9392)
* fix(serve): let channel workers reach TLS-enabled daemons

The channel worker supervisor always handed workers an http:// loopback
URL, and workers rejected any other scheme, so on a daemon started with
--tls-cert/--tls-key the worker's first capabilities fetch hit the
HTTPS-only listener as plain HTTP and died with "fetch failed" before
reporting ready ("Channel worker exited before ready (code=1)").

- Emit an https:// loopback URL for the worker when TLS is configured
- Accept https loopback in the worker's QWEN_DAEMON_URL validation
- Inject NODE_EXTRA_CA_CERTS with the daemon cert into the worker env
  (merged with an operator-set value, since it accepts a single file)

* fix(serve): make the worker TLS trust injection actually establish trust

Round 1 review found three ways the CA injection this PR adds silently
fails to give channel workers a usable trust anchor (R1-1, R1-2, R1-3),
plus the diagnosability and coverage gaps around it (R1-4..R1-8).

- R1-1: `--tls-cert` was forwarded to the worker verbatim. Workers are
  forked with `cwd: opts.workspace`, so a relative path resolved against
  the worker's cwd instead of the daemon's, Node silently ignored the
  unloadable extra cert, and every handshake failed
  DEPTH_ZERO_SELF_SIGNED_CERT — the exact pre-PR symptom. Resolve once at
  the source, next to the read that already validated it.
- R1-2: the merged CA bundle went to `os.tmpdir()/qwen-worker-ca-<pid>.pem`,
  a path predictable from the daemon PID (CWE-377/CWE-59). A pre-planted
  symlink redirected the write; a pre-planted regular file kept attacker
  ownership and mode while receiving the full cert — the private key too,
  for a combined PEM. Write into an `mkdtempSync` 0700 directory instead,
  the same defence standalone-update.ts already uses in this tmpdir.
- R1-3/R1-4: a serving cert only anchors trust when it signed itself, and
  only reaches the worker when its SANs cover the loopback host workers
  dial. Neither held for the `mkcert` flow this project documents, and
  boot validation checked parse/expiry/validity-window only — so the
  daemon booted green, browsers connected, and every worker restart-looped
  with /health still green. `describeWorkerTlsTrustGaps` names both at
  boot, the way the adjacent expiry guard does. The non-self-signed check
  stays quiet when the operator set NODE_EXTRA_CA_CERTS, since that value
  is merged into the worker bundle and may already carry the issuing root.
- R1-5: the merge-failure `catch` dropped the operator-set
  NODE_EXTRA_CA_CERTS with no diagnostic, and Node stays silent when the
  remaining cert loads fine. Emit a process warning naming both paths.
- R1-6: the bundle was never cleaned up. Merged bundles are now memoized
  per (operator CA, daemon cert) pair — workers respawn on every restart,
  so minting a directory per spawn would leak one per restart — and
  removed on daemon exit.
- R1-7/R1-8: tests for the merge-failure fallback and for the
  `workerTlsCaCertPath` pass-through, plus an end-to-end test that boots
  the daemon with a relative `--tls-cert` and asserts the supervisor gets
  an absolute path and an https daemon URL.

Verification: every fix was mutation-checked — reverting `path.resolve`,
the mkdtemp write, the trust-gap detection, the merge-failure warning, the
group pass-through, and the bundle memoization each turns at least one new
test red. `npx vitest run src/serve/run-qwen-serve.test.ts
src/serve/channel-worker-supervisor.test.ts
src/serve/channel-worker-group.test.ts` → 403 passed. eslint and prettier
clean on the six touched files.

* test(serve): declare the worker TLS trust check's NODE_EXTRA_CA_CERTS reads

The `Test (ubuntu-latest, Node 22.x)` job failed on 04c954dcb9 with a single
red test: `serve process.env guard > allows only documented process-scoped
process.env expressions`. 04c954dcb9 added the worker TLS trust-gap check,
which reads `process.env['NODE_EXTRA_CA_CERTS']` twice in
run-qwen-serve.ts (once to test for it, once to pass it), but did not add
the matching entry to `allowedProcessEnvAccesses`. The guard is an explicit
allowlist, so any undeclared process-scoped read is a failure by design.

Declare `key:NODE_EXTRA_CA_CERTS: 2` and record why this particular read is
process-scoped rather than request-scoped: NODE_EXTRA_CA_CERTS is the trust
store Node already loaded for this process, so the check has to consult the
same value to know whether the operator has already supplied the issuing CA.

Mutation-verified: with the count at 1 instead of 2 the guard test goes red
with the same mismatch shape, so the allowlist is genuinely counting the
occurrences and not just matching the key.

* fix(serve): judge the worker TLS trust gaps on the whole serving file

R2-1, R2-2, R2-5 from review round 2.

R2-1. `workerDialHost` returned WHATWG `URL.hostname`, which keeps the brackets
on an IPv6 literal (`[::1]`). `isIP('[::1]')` is 0, so `certCoversHost` took the
DNS-name branch and `checkHost('[::1]')` could never match the iPAddress SAN the
certificate actually carries — the boot diagnostic false-positived on every TLS
daemon bound to `::1` with a correct cert, and told the operator to reissue it.
The brackets are now stripped, so the address is checked as an address and also
printed unbracketed the way a SAN spells it.

R2-2. `describeWorkerTlsTrustGaps` built one `X509Certificate` from the file,
which reads only the FIRST PEM block. A standard `fullchain.pem` (leaf +
issuing CA) was therefore judged on its leaf alone and reported as unable to
anchor worker trust — even though the supervisor injects that same whole file
as the workers' `NODE_EXTRA_CA_CERTS`, root included, so trust does establish.
The file is now split into every certificate it carries and the leaf's chain is
walked through them; the gap is reported only when the chain fails to terminate
in a self-signed certificate inside the file. A leaf-only file still reports it.
The walk is bounded by a fingerprint set, so a cross-signed pair cannot loop.

R2-5. The merged-CA-bundle test asserted `toContain('OP-CERT')` +
`toContain('DAEMON-CERT')`, which both survive mutating the join separator to
`''` — with real PEM inputs that mutant fuses `-----END CERTIFICATE-----` onto
the next `-----BEGIN CERTIFICATE-----` and makes the bundle unparseable. It now
asserts the exact bundle text, which pins the separator and the order.

Verified: run-qwen-serve 275/275, channel-worker-supervisor 90/90, eslint and
prettier clean on the touched files. Typecheck error count is 139 both with and
without this change (worktree build skew against the main checkout's stale
`@qwen-code/*` dist; the same 139 appear on the unmodified branch).
Mutation-checked three ways, each reverting exactly one fix:
  - dropping the bracket strip fails both new IPv6 tests
  - `chainIsSelfAnchored` -> `isSelfSignedCert` fails the fullchain test
  - `.join('\n')` -> `.join('')` fails the merged-bundle test

* fix(serve): judge the worker CA bundle by what Node's loader accepts

Round 2 review findings on #9392: 2 Critical, 5 Suggestion.

R2-11 (Critical): the merge treated a merely *readable* operator
NODE_EXTRA_CA_CERTS as trustworthy. Node's certificate loader is
line-strict and all-or-nothing — a bundle built with
`cat a.pem b.pem` where a.pem lacks a trailing newline fuses
`-----END CERTIFICATE----------BEGIN CERTIFICATE-----` onto one line,
and Node then discards the WHOLE bundle, daemon cert included. The
existing fallback only fired on a read *failure*, so this shape sailed
through the success path and left every worker trusting neither the
operator CA nor the daemon cert while /health stayed green. The merge
now extracts blocks with a line-strict PEM matcher and takes the
existing warn-and-fall-back path when the operator file yields no
loadable block or has a marker that produced none.
`tls.createSecureContext({ ca })` does not throw on that shape, so it
is not used as the validator.

R2-12 (Critical): guard the 0o700 bundle-directory mode assertion on
win32. `fs.mkdtempSync` ignores the mode there and libuv synthesises
st_mode from file attributes (0o666 for a writable directory,
structurally never 0o700), so the merge queue's test_windows job would
go red on a test that passes on Linux/macOS. Same guard shape as
observed-contact-store.test.ts.

R2-13: write only certificate blocks into the bundle. A combined
cert+key serving PEM passes boot validation, which parses the first
block alone, so its private key was being copied into a tmpdir file
NODE_EXTRA_CA_CERTS never reads — and that copy outlives a SIGKILLed
daemon, whose `exit` cleanup cannot run.

R2-4: revalidate the merged-bundle cache. It was keyed on paths alone,
so an in-place operator CA rotation never reached respawned workers for
the daemon's whole lifetime (before this PR a respawn read the
operator's file live), and an external tmp cleaner aging out the bundle
directory left every future respawn pointed at a dead path. Cache
entries now carry each source's mtime/size and the bundle's existence
is re-checked on hit.

R2-3: harden the boot-time trust-gap check along the three corners the
review demonstrated, per its stated minimum. Coverage is judged on the
operator CA's *contents* rather than on the variable being set; every
member of the anchor walk has its validity window checked
(`x509.verify` is signature-only and never consults dates, so an
expired root anchored "fine" while every handshake failed
CERT_HAS_EXPIRED); and the leaf-anchor message no longer asserts a
certain failure, since the check cannot see the workers' default trust
store. `chainIsSelfAnchored` becomes `walkWorkerAnchorPath`, which
returns the certificates the walk relied on so the date check can scope
itself to them.

R2-14: pin worker-side acceptance of `https://[::1]:4170`. The formatter
emits it for a `::1` TLS bind and nothing else pinned the `'[::1]'`
entry in LOOPBACK_BINDS, so dropping it as redundant kept every test
green while regressing this PR's own failure mode on IPv6.

R2-6: cover the boot-time warning wiring end to end. Only the pure
function was tested, so deleting the loop, inverting its guard or
feeding it unresolved values all shipped green. Two runQwenServe tests
now boot a real TLS daemon on `::1` (a real SAN gap for a fixture cert
that still pairs with its key) and on 127.0.0.1, asserting the gap text
does and does not reach the daemon log.

BEHAVIOUR FLIP — leaf-anchor gap suppression. A set-but-unhelpful
NODE_EXTRA_CA_CERTS used to silence this warning outright. It no longer
does: a typo'd, unrelated or unloadable path anchors exactly as little
as no CA at all, and suppressing on the variable's mere presence
silenced the diagnostic in the cases it was written for. The test that
pinned the old behaviour is rewritten to assert the new contract rather
than deleted, and three tests cover the paths it used to hide
(anchoring CA, non-anchoring CA, unreadable path).

BEHAVIOUR FLIP — a DER-encoded operator NODE_EXTRA_CA_CERTS is now
refused with a warning instead of concatenated. Node's loader rejects
it either way; the difference is that it no longer takes the daemon
cert down with it.

Verification: packages/cli — run-qwen-serve (283), channel-worker-
supervisor (94), daemon-worker (85), process-env-guard (3),
channel-worker-group — 507 tests pass. eslint and prettier clean.
`tsc --noEmit -p packages/cli` reports 2 errors, both TS6305 against
packages/core/dist; the same 2 appear on the stashed tree, so they are
worktree build skew, not this change. Mutation-verified, 11 of 11
mutants killed: loose PEM regex, whole-file copy (key retained), no
source-stamp revalidation, no bundle stat, `'[::1]'` dropped from
LOOPBACK_BINDS, path-only gap suppression, chain-date check deleted,
unsoftened wording, warn loop gutted, warn guard inverted, wrong
daemonUrl fed to the check. R2-12 is a test-only platform guard with no
production code to mutate.

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

* fix(serve): refuse a non-CA chain terminator and document the worker TLS hop

Clears the four findings still open on #9392 from earlier rounds that
round 2 did not re-report inline.

R2-10: `chainIsSelfAnchored` modelled chain geometry only. OpenSSL also
requires a certificate that SIGNS others to carry
`basicConstraints CA:TRUE`, so a fullchain of leaf + self-signed
CA:FALSE issuer was blessed as anchored while every worker handshake
failed INVALID_PURPOSE — boot green, no warning, the exact silent
outage this diagnostic exists to name. Measured on Node 22 with a real
`tls.connect`: leaf + CA:FALSE self-signed issuer as the trust store →
`INVALID_PURPOSE: unsuitable certificate purpose`.

The constraint binds only PAST the leaf. The same probe shows a
CA:FALSE self-signed cert in its OWN trust store is verified at depth 0
and handshakes fine (`authorized=true`) — which is what plain
`openssl req -x509` produces — so requiring CA:TRUE there would cry
wolf on the ordinary self-signed daemon cert. `walkWorkerAnchorPath`
now rejects a non-CA terminator only when the walk took at least one
step, and reports it separately so the gap text names INVALID_PURPOSE
and the CA:FALSE remedy rather than UNABLE_TO_VERIFY_LEAF_SIGNATURE.
R2-10's other shape — an expired self-signed root — is already covered
by the chain-date check added in the previous commit.

Two fixtures back this: a leaf signed by a self-signed CA:FALSE issuer,
and a self-signed CA:FALSE leaf with loopback SANs. Both were minted
with OpenSSL 3.0.13 and are the exact files the handshake probes above
ran against.

R2-7: no case drove the function to a two-gap outcome, so an inserted
`return gaps` after the first push — or turning the SAN `if` into an
`else if` — survived the whole suite. Under that mutant an operator
fixes the trust anchor, restarts, and only then meets the SAN failure.
Added a CA-issued cert dialled at a host its SANs miss, asserting both
error names.

R2-8: the documented mkcert flow produces a CA-issued leaf — precisely
the shape the new boot warning flags — but the docs never connected
channel workers to TLS (`grep -c NODE_EXTRA_CA_CERTS
docs/users/qwen-serve.md` → 0). Added the HTTPS/TLS note: workers dial
the daemon back over https, self-signed certs and self-carrying
fullchains need nothing, the mkcert flow needs
`NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem"` exported in the
daemon's launch environment, and an operator-set value is merged with
the daemon cert rather than replacing it.

R2-9: documented the rotation asymmetry on the `tlsCaCertPath` option,
per the finding's stated minimum. With no operator CA the worker gets
the `--tls-cert` PATH and Node re-reads it at every respawn while the
daemon still serves its boot-time bytes, so an in-place rotation makes
respawned workers restart-loop; with an operator CA the merged bundle
pins a snapshot instead. Either way the rotation needs a daemon
restart, now said in both the JSDoc and the serve docs.

Verification: packages/cli — 510 tests pass across run-qwen-serve
(286), channel-worker-supervisor (94), daemon-worker (85),
process-env-guard (3) and channel-worker-group. eslint clean; prettier
clean including docs/users/qwen-serve.md. `tsc --noEmit -p
packages/cli` reports the same 2 pre-existing TS6305 errors against
packages/core/dist that the stashed tree reports — worktree build skew,
not this change. Mutation-verified, 3 of 3 new mutants killed: CA check
removed, CA check applied to the leaf as well, and the SAN gap
suppressed once a trust-anchor gap exists.

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

* fix(serve): judge worker CA files with the loader's own rules, on both sides

Round 3 review: 2 Critical (R3-1, R3-2) and 6 Suggestions (R3-3..R3-8).

R3-1 (Critical) — `extractCertificateBlocks` diverged from Node's
NODE_EXTRA_CA_CERTS loader in both directions. Too lax: it validated block
*shape* only, so a body of base64 characters that does not decode was merged
ahead of the daemon cert, and Node then discarded the WHOLE bundle — workers
lost trust in the operator CA *and* the daemon cert while /health stayed
green. Each block is now parsed with `X509Certificate`, the loader's own
parser. Too strict: a UTF-8 BOM, trailing whitespace after a marker line and
leading whitespace on body lines were all rejected into the daemon-cert-only
fallback with a warning that misdiagnosed the file; they are normalised away
before matching.

R3-2 (Critical) — the boot-time trust-gap diagnostic modelled the operator's
CA with a looser parser than the spawn-time merge: a fused-marker bundle, or a
DER file NODE_EXTRA_CA_CERTS never reads, was counted as an anchoring CA at
boot while the merge discarded it and handed workers the daemon cert alone.
The daemon log stayed clean and every worker handshake failed
UNABLE_TO_VERIFY_LEAF_SIGNATURE — the exact silence this diagnostic exists to
end. Both sides now share one extractor, moved to `pem-certificate-blocks.ts`,
and an unloadable operator file is named in a gap instead of being trusted.

R3-8 (behaviour flip) — `X509Certificate.ca` reads false both for an explicit
`basicConstraints CA:FALSE` and for a v1/no-extension root, but OpenSSL
accepts the second as an issuer. The INVALID_PURPOSE boot warning therefore
fired on legacy anchors that work, telling operators to reissue a working CA.
It now fires only when the certificate carries the extension and declares
CA:FALSE. Measured on Node 22 / OpenSSL 3: a leaf anchored by a v1 root
handshakes authorized=true, while the explicit CA:FALSE twin really does fail
INVALID_PURPOSE.

R3-5 — `warnWorkerCaMergeFallback` re-emitted on every spawn, so a
crash-looping worker buried the log stream the operator reads to diagnose it.
Deduped per path pair, keyed on the paths alone so flapping errno text cannot
defeat it.

R3-3 — the `tlsCaCertPath` comment claimed an operator CA pins a snapshot and
makes in-place `--tls-cert` rotation invisible to workers. The code does the
opposite: `resolveWorkerCaCertPath` stamps both sources, so rotation rebuilds
the bundle from the new contents. Corrected to match the code and
docs/users/qwen-serve.md:381.

R3-6 — the probe is right that no test kills
`mergedWorkerCaBundles.delete(cacheKey)`, but no test can: control always
reaches the rebuild, which overwrites the key on success, and every hit
re-stats the bundle and re-compares both stamps before returning it. The
statement could not change an observable result, so it is removed rather than
pinned by a test that would pass without it. The eviction *behaviour* stays
covered by the rotation and tmp-cleaner tests.

R3-4, R3-7 — new coverage: a CRLF operator bundle, a BOM operator bundle, a
marker/body-whitespace bundle, an undecodable block, warn-once-per-pair, and
three boot-log tests that drive the `process.env['NODE_EXTRA_CA_CERTS']` read
and its try/catch end to end through `runQwenServe`.

Every fix was mutation-verified: reverting each one turns exactly its own
test(s) red (9 mutants, 9 kills). The loader claims above were measured
against a real NODE_EXTRA_CA_CERTS handshake on Node 22.23, not inferred.

* fix(serve): judge worker CA framing the way Node's loader does

Round 4 review of #9392: four Critical findings, three of them rooted in
the same place — this code re-implemented Node's `NODE_EXTRA_CA_CERTS`
loader instead of following it.

R4-2 (Critical): `extractCertificateBlocks` pattern-matched what a
well-formed PEM file looks like, and a new divergent shape surfaced in
each of the last three rounds. Replaced with a line scanner that walks
the file the way OpenSSL's `PEM_read_bio_X509` loop does. Three shapes
Node loads and this rejected now extract: a `-----BEGIN CERTIFICATE-----`
substring embedded in a line of prose (markers are matched at line start,
not as unanchored substrings), whitespace inside a base64 body line, and
a UTF-8 BOM in front of a block that is not the first in the file (what
concatenating operator files produces). Every one of them silently fell
back to daemon-cert-only while telling the operator the file "holds no
PEM certificate block Node can load".

BEHAVIOUR FLIP — the loader is prefix-loading, not all-or-nothing. The
doc comment this module carried claimed a malformed block discards the
whole bundle. Measured on Node 22 / OpenSSL 3 through real
`NODE_EXTRA_CA_CERTS` handshakes: a good root followed by a fused block
still handshakes `authorized=true` while Node prints `Ignoring extra
certs … bad end line`. The loader keeps every certificate up to the first
malformed block and loses that block and everything after it. So does
this now; returning `undefined` for the whole file threw away anchors the
workers do in fact receive. The fused-file and bad-decode cases still
return `undefined`, because there the bad block IS the first one.

Both behaviours were taken from the loader, not inferred: 15 shapes were
written to disk, pointed at through `NODE_EXTRA_CA_CERTS` in a child
process, and checked against a real `tls.connect` to a server holding the
leaf they anchor. The parser agrees with the oracle on all 15, and
`pem-certificate-blocks.test.ts` (new — this module had no direct
coverage, which is how three rounds of shapes got through) pins each one
with the measured verdict in the comment.

R4-4 (Critical): `walkWorkerAnchorPath` applied the CA-suitability check
only to the self-signed terminator, so a chain passing THROUGH an
incapable issuer was reported anchored while every worker handshake
failed. Issuer capability is now required of every non-self-signed chain
member the walk leans on. Measured with real handshakes: a CA:FALSE
intermediate and a v3 intermediate with no basicConstraints both fail
INVALID_PURPOSE, and a keyCertSign-only intermediate fails INVALID_CA —
all three reported gaps=NONE before. The self-signed terminator keeps its
existing, looser rule, so the v1 root and CA:FALSE self-signed leaf cases
stay unflagged as measured in earlier rounds.

R4-3 (Critical): the boot diagnostic modelled a merged serving+operator
trust store that the workers never receive when the serving file fails
extraction — `resolveWorkerCaCertPath` finds `daemonBlocks === undefined`,
discards the operator CA and hands them the serving file alone. Boot
reported no gap while every worker handshake failed. The model now
mirrors the fallback and names the discarded operator CA. The comment's
premise (that such a file "cannot serve at all") was false and is gone.

R4-1 (Critical): every `writeMergedWorkerCaBundle` call registered its
own `process.once('exit')` listener. The merge cache is invalidated on
purpose by in-place operator CA rotation and by tmp-cleaner aging, so a
long-lived daemon accumulated a listener, a closure and an orphaned
bundle directory per rebuild, and past the tenth printed
`MaxListenersExceededWarning` into the log stream the fallback dedup
exists to keep readable. One module-level hook now cleans up every minted
directory, and a rebuild removes the directory it supersedes.

R4-5 (Suggestion): the fallback-warning dedup was keyed on the path pair
and add-only, so the first failure silenced every later one. Keyed on a
coarse failure family now, and the keys are lifted when the pair merges
successfully — a changed failure mode and a relapse after a fix are both
new information.

R4-6 (Suggestion): the fallback message blamed markers alone, but this
PR's own X509 decode gate added a third rejection cause. Aligned with the
boot-side wording, which already enumerates all three.

R4-7 (Suggestion): the DER and fused operator-CA tests asserted gap
presence via `.some()` without pinning the count, and never asserted the
DER-specific text. Both now pin `toHaveLength(2)`, and the DER test
asserts its own message.

Every fix is mutation-verified: reverting it turns at least one test red
(9 mutants run, 9 killed).

Verification: `npx vitest run src/serve/pem-certificate-blocks.test.ts
src/serve/channel-worker-supervisor.test.ts
src/serve/run-qwen-serve.test.ts` — 411 passed; channel-worker-group /
-manager / -diagnostics — 84 passed; eslint and prettier clean on the six
touched files. `npm run build` and `npm run typecheck` do not complete in
this worktree for reasons that predate this change and reproduce with it
stashed (a `sharp` typing skew in packages/core and `@qwen-code/*`
resolving to the sibling checkout's dist): 105 typecheck errors with and
without the change, none in the touched files.

* fix(serve): judge a chain terminator and a marker line the way OpenSSL does

Round 5's three Critical findings, each measured against a real handshake on
Node v22.23.0 / OpenSSL 3.0.13 before and after.

R5-1 — the self-signed-terminator check read basicConstraints' PRESENCE, so a
v3 root carrying only a subjectKeyIdentifier (`.ca === false`, no
basicConstraints OID, no keyCertSign — a minimal `openssl req -x509` config)
was reported anchored while OpenSSL refuses it as an issuer: measured
`authorized=false code=INVALID_PURPOSE` with the boot log, /health and the
daemon all green and every worker restart-looping. Replaced with
`cannotIssueCertificates`, which mirrors `check_ca()` in `v3_purp.c` in the
same order — keyUsage first, then basicConstraints, then the v1-root and
keyCertSign exemptions — reading the extensions out of the DER through a real
element walk instead of scanning `cert.raw` for OID bytes that also occur
inside a signature. Six shapes measured, all six agree: v3/SKI-only refused,
keyCertSign-only accepted, CA:TRUE+keyCertSign accepted, v1 root accepted,
CA:TRUE with keyUsage lacking keyCertSign refused, CA:FALSE with keyCertSign
refused.

R4-2 — `normalizePemLine` stripped LEADING whitespace before the marker match,
so a CA file whose `-----BEGIN/END CERTIFICATE-----` markers are indented was
counted anchorable. Node's loader takes nothing from such a file (measured:
`UNABLE_TO_VERIFY_LEAF_SIGNATURE`, no `Ignoring extra certs` warning, and
`openssl storeutl -certs` reports 0), while the same file un-indented
handshakes `authorized=true`; trailing whitespace, CRLF and a BOM in front of
the marker all load and stay tolerated. The marker match is now anchored at
column 0, which is also what `pemMarkerLabel`'s own doc already claimed.
The same finding's fifth entrance is closed too: the loader decodes EVERY
block's body whatever its label and stops the file on a bad decode, so a
corrupt or empty leading PRIVATE KEY block now stops the scan instead of being
skipped unvalidated (measured on both shapes).

R5-17 — `hands workers an absolute --tls-cert path` asserted that
`path.relative(process.cwd(), certPath)` is relative, with the cert minted
under `os.tmpdir()`. On the required merge-queue job `Test (windows-latest,
Node 22.x)` the workspace is on D: and `os.tmpdir()` on C:, where cross-drive
`path.relative` returns the ABSOLUTE target and the precondition fails —
verified through `path.win32`. `TMPDIR` cannot move it (win32 `os.tmpdir()`
reads TMP/TEMP/USERPROFILE). The fixture now falls back to a directory under
the vitest cwd exactly when the relative path comes back absolute, so the Linux
path is unchanged.

Round 5's Suggestions, all pinned by tests whose mutants were measured green
beforehand:

- R5-2: the `no-daemon-blocks` fallback had no test. Added one built on a
  serving PEM whose first block lacks its END line followed by a complete
  block — accepted by `tls.createSecureContext`, so the daemon boots and
  serves, while the loader takes nothing.
- R5-5 / R5-6 / R5-27: the exit hook's body is now
  `cleanupMintedWorkerCaBundleDirs()`, exported and returning what it swept.
  One test pins that exactly one such listener is registered, that a
  superseded bundle has already left the registry, and that the sweep empties
  it. Deleting the registration, the `delete` or the `clear` each turn it red;
  before, deleting the whole `process.once('exit', …)` registration left all
  103 tests green.
- R5-9: the minted directory is registered before the bundle write, not after,
  so a write that throws (ENOSPC/EDQUOT on a size-capped tmpfs) leaves a
  directory the exit hook can still see rather than an untracked 0700 orphan
  per failing respawn. No test: forcing that write to fail needs `node:fs`
  mocked file-wide, which this suite cannot do without changing how its other
  104 tests resolve fs.
- R5-26: added a leaf ← v1 intermediate ← CA:TRUE root fixture. Narrowing the
  intermediate check to the terminator's test shipped green before it.
- R5-28: added a key-BEFORE-cert file. A stop-at-first-non-certificate mutant
  shipped green before it; the loader skips the key block and loads the cert.

Verification: `packages/cli` — pem-certificate-blocks (19), run-qwen-serve
(298), channel-worker-supervisor (105) and daemon-worker (85), 507 passing.
ESLint and Prettier clean on the six touched files. `tsc --noEmit` reports the
same 105 errors before and after the change; all of them are the worktree's
stale `@qwen-code/acp-bridge` dist, none in these files.

* test(serve): pin the failed-mint registry order R5-9 left untested

`4a935b38fa` fixed R5-9 — the merged-bundle directory is registered before the
write, not after — and stated it could not pin it: forcing `writeFileSync` to
throw looked like it needed `node:fs` mocked file-wide, which would change how
the other 105 tests in that suite resolve fs.

It does not need that. `vi.doMock` is not hoisted, so it binds only to the
dynamic `import()` beside it: that one supervisor instance sees a throwing
`writeFileSync` while every other test in the file keeps the real `node:fs` it
imported at load. (`vi.spyOn(fs, 'writeFileSync')` is the approach that cannot
work here — an ESM module namespace is not configurable.)

The test drives a spawn whose bundle write fails with ENOSPC, then asserts the
three things the fix is about: workers fall back to the daemon cert alone,
exactly one directory was minted and is still on disk, and
`cleanupMintedWorkerCaBundleDirs()` returns it and removes it.

Mutation-verified against the pre-fix order: moving
`mintedWorkerCaBundleDirs.add(dir)` back below the write turns this test red
and leaves the other 105 green — which is the finding's own claim about what
the suite could not see.

Verification: `channel-worker-supervisor.test.ts` 106 passed. ESLint, Prettier
and `tsc --noEmit -p packages/cli` clean on the file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(serve): frame worker CA files and judge chains the way OpenSSL does

Round 6 reported seven Criticals against the worker TLS trust surface. Each
fix below is measured against the real oracle — a `tls.connect` from a child
process holding the file under test as `NODE_EXTRA_CA_CERTS`, against a server
serving the leaf, on Node v22.23.0 / OpenSSL 3.0.13 — and mutation-verified.

R4-2, the class finding, is closed structurally rather than entrance by
entrance. `extractCertificateBlocks` now models the loader's own framing
decisions instead of re-deriving what a well-formed file looks like:

- the certificate label alias set is {CERTIFICATE, X509 CERTIFICATE}
- a block is `header CRLF CRLF data`; the first blank line splits it, and a
  header section without `Proc-Type` fails the load (`not proc type`) while one
  with it is a key the loader consumes and reads past
- a BOM is tolerated only in front of a BEGIN marker, which is the only
  position measured to load; in front of an END marker, or inside a base64
  line, the loader takes nothing
- non-certificate bodies are judged by a strict base64 predicate, not by the
  alphabet alone (`====` and `AAAAA` are alphabet-valid and both take the
  whole file down)

Sixteen shapes were measured against real handshakes and the module now agrees
with the loader on all sixteen, including one divergence (a BOM inside a base64
line) that predates this round.

The remaining six are in the boot-time diagnostic:

- R6-1: an unreadable serving file is a gap whether or not an operator CA is
  set. Gating it on `operatorChain` reported zero gaps on the no-operator path
  while every worker restart-looped.
- R6-2: the leaf every downstream check judges is the one BOOT parsed, not the
  first match of an unanchored regex. A block whose BEGIN line is indented is
  prose to the column-0 readers but matched the regex, so the SAN, expiry and
  issuer checks judged a certificate the daemon never serves.
- R6-3: in the discard scenario the operator CA anchored nothing only because
  it was thrown away with the unloadable serving file. Saying its contents "do
  not carry a certificate that anchors it" is false, and its remedy is a no-op
  when the variable already points at the issuing CA.
- R6-4: an unreadable `NODE_EXTRA_CA_CERTS` file is now named as unreadable,
  with its error code, instead of being downgraded to "no contents" — the old
  message asserted an unknowable content fact and prescribed an action the
  operator had already taken, when the real fix is permissions.
- R6-5: issuers are found by name match plus signature, and the capability
  judgment runs separately. `checkIssued` enforces the issuer's keyUsage, so
  using it as the SEARCH predicate meant a CA:TRUE intermediate without
  `keyCertSign` was never found and the walk fell through to a generic gap
  whose cause, error code and remedy are all wrong for that shape.
- R6-6: `pathLenConstraint` is modelled. It sits inside the same
  basicConstraints value the capability checks already read, and went unread —
  a `pathlen:0` root over one intermediate walked to `anchored: true` with zero
  gaps while every worker handshake failed PATH_LENGTH_EXCEEDED.

Verification: 29/29 pem-certificate-blocks, 305/305 run-qwen-serve, 5246 passed
across `src/serve` with the same 3 pre-existing root-permission failures as the
unmodified branch; eslint clean; typecheck unchanged at 105 pre-existing
module-resolution errors. Seventeen mutation arms — one per fixed behaviour,
plus an off-by-one on the new constraint check — each turn a test red.

* fix(serve): name the cause a refused anchor actually has

Round 7 of the review found both remaining boot-gap messages asserting a
cause and an outcome the measured chain does not have.

R7-1: `cannotIssueCertificates` refuses a self-signed terminator for three
independent reasons — keyUsage without keyCertSign (whatever
basicConstraints says), basicConstraints present with `!ca`, and the
non-v1 no-basicConstraints shape — and the `nonCaTerminator` message
described only the second. A root minted as `basicConstraints critical
CA:TRUE` + `keyUsage critical digitalSignature` was told it "carries
basicConstraints CA:FALSE" (false), that handshakes fail INVALID_PURPOSE,
and to "Reissue that certificate with CA:TRUE" — which it already is. The
other offered remedy cannot work either: nothing but itself anchors a
self-signed certificate. Both remedies being no-ops, the operator loops
reissue/restart with no usable guidance. Split the branch on
`issuerRefusedForKeyUsage`, the same way the sibling `incapableIssuer`
branch already does, and widen the remaining arm to cover the
no-basicConstraints shape it also fires on.

Measured on Node v22.23.0 / OpenSSL 3.0.13 with the new fixture: `openssl
verify` reports `error 32 ... key usage does not include certificate
signing`, and a real worker-shape handshake (fullchain as the trust store)
fails with that same text — not INVALID_PURPOSE. The message now says so.

R7-2: the NODE_EXTRA_CA_CERTS read-error gap announced a certain
UNABLE_TO_VERIFY_LEAF_SIGNATURE outage that does not happen when the
serving file anchors itself. `resolveWorkerCaCertPath`'s catch hands each
worker the serving file as its extra-CA store, so a fullchain — certbot
and mkcert's normal shape — loads its own root and every handshake
succeeds, while the anchor walk in this very function returns
`anchored: true` for exactly that shape. The diagnostic knew the config
worked and announced an outage anyway; its only hedge covered the workers'
DEFAULT trust store, not the CA the serving file itself carries. The one
test setting `operatorCaCertReadError` used a leaf-only serving file,
where the claim happens to hold. The gap is now pushed after the anchor
walk and its failure sentence is conditional on the chain not anchoring;
the serving-file gap moved with it so the emitted order is unchanged.

Behaviour flip: both messages change text an operator reads at boot. The
keyUsage terminator now names keyUsage rather than basicConstraints and
predicts the measured error text rather than INVALID_PURPOSE; the
read-error gap stops predicting a handshake failure when the serving chain
anchors. No test pinned the old claims for these shapes — no fixture
exercised a keyUsage-refused terminator at all, and the CA:FALSE
terminator test still asserts INVALID_PURPOSE unchanged.

Verification: `npx vitest run src/serve/run-qwen-serve.test.ts` -> 307
passed. Mutants, each red on exactly one new test: force the R7-1 split to
the CA:FALSE arm; force the R7-2 claim unconditional; force it always
anchored (caught by the leaf-only arm, which pins that the outage sentence
still fires where it is true). `npx tsc -p tsconfig.json --noEmit` reports
5 errors with and without this change, all in unrelated files.
`npx eslint` clean on both. `npx vitest run src/serve/` -> 5248 passed,
3 failed; the same 3 fail on the stashed tree (chmod-based tests that
cannot constrain uid 0).

* fix(serve): read a headed PEM block the way the loader's own label rules do

Closes the two Criticals of review round 8.

R4-2 (pem-certificate-blocks.ts): `extractCertificateBlocks` enforced RFC
1421's "the first header must be `Proc-Type`" rule for blocks of EVERY label,
while the `NODE_EXTRA_CA_CERTS` loader inspects a header section only on a
block it tries to consume — and it consumes certificate labels alone. An
operator CA file holding, say, a `PRIVATE KEY` block whose header section
starts with `Comment:` therefore loaded fine for the workers themselves
(handshake `authorized: true`) while this scan returned `undefined`, so
`resolveWorkerCaCertPath` fired its no-operator-blocks fallback, discarded the
operator CA, handed workers the daemon cert alone and blamed marker/decode
defects the file does not have. Pre-PR the env value reached workers
untouched, so this was a regression against the PR's own "merged, not
replaced" contract.

Rather than close that entrance alone, the header branch now follows the two
rules the loader was measured to actually have, which closes the round's other
two reported divergences with it:

- A header section on a CERTIFICATE-family block stops the file whatever it
  says. `Proc-Type` does not spare it — the loader goes on to decrypt and
  aborts `bad decrypt` (with `DEK-Info`) or `not dek info` (without). Such a
  block used to be SKIPPED, so the scan read straight past a stop.
- The body BELOW a header section is still decoded for every label, so an
  encrypted key with an undecodable body is `bad base64 decode` and stops the
  file. The old branch `continue`d before the base64 judgment and reported
  certificates behind that stop as anchors the workers never got.

R8-1 (run-qwen-serve.ts): `describeWorkerTlsTrustGaps` assumed
`servingBlocks[0]` is the served leaf. A serving file whose leaf carries the
`TRUSTED CERTIFICATE` label (what `openssl x509 -trustout` writes) followed by
its root yields `servingBlocks = [root]`, so the anchor walk started at the
root, at depth 0, where the leaf-depth exemption waives the CA-capability
check — the walk returned anchored and the diagnostic reported zero gaps while
every worker handshake failed. Boot stays green throughout: `X509Certificate`
reads the trusted label and `createSecureContext` serves the file. The walk now
anchors at the certificate boot parsed whenever `servingBlocks` does not
contain it, mirroring the `servingBlocks === undefined` fallback beside it.

Every rule above was measured on Node v22.23.0 / OpenSSL 3.0.13 through real
`NODE_EXTRA_CA_CERTS` handshakes in the worker shape before it was written
down, including the quiet controls: a capable root over a label-hidden leaf
authorizes and the diagnostic stays silent, and a well-formed legacy encrypted
key is still read past to the certificates behind it.

Verification: `vitest run src/serve/` — 5252 passed, 3 failed; the same 3 fail
on the unmodified branch (5248 passed) and are the known root-uid failures
where `chmod` cannot block a read or unlink. Each of the five fixes was
mutation-verified by reverting it alone, and each turned at least one test red.

* fix(serve): judge a PEM block the way the loader's own parser does

R4-2 and R8-1 of round 9, both measured against Node v22.23.0 with real
`NODE_EXTRA_CA_CERTS` handshakes rather than reasoned about.

R4-2, two divergences from the loader in `extractCertificateBlocks`:

- The X509 gate parsed the re-rendered PEM, which is stricter than the
  loader by exactly one shape: a body carrying a complete DER certificate
  followed by extra bytes. `new X509Certificate(<that PEM>)` throws
  `wrong tag`; the loader TAKES the block (`authorized: true`, no
  `Ignoring extra certs` warning, 3 trailing bytes appended to a root).
  The gate now parses the decoded bytes, which accept what the loader
  accepts and still throw on truncated or invalid DER. Judging the PEM
  dropped that block and every block behind it, so the merge discarded a
  CA the workers' own loader reads and the operator was told the file
  holds no loadable certificate block.

- A BEGIN marker inside a body was folded into the body, where the
  base64 judgment failed on its `-` characters and dropped the WHOLE
  file. The loader ends the block there, takes what it collected, and
  reads nothing further. Four measured shapes pin both halves:
  `[root without its END line][full root]` authorizes with no warning
  (the truncated body is taken); `[leaf without its END line][full
  root]` fails UNABLE_TO_VERIFY_LEAF_SIGNATURE with no warning (the root
  BEHIND it is not taken, so the loader stops rather than resuming at
  the marker); `[full leaf][leaf without its END line][full root]`
  likewise; and an unclosed block at EOF is still `bad end line`.

R8-1, the trust-gap diagnostic was blind to a self-signed served leaf
the workers never receive. The fingerprint check only decides whether to
prepend the boot-parsed leaf to the modeled worker store; once prepended,
a self-signed leaf self-anchored the walk at path length 1 and boot
reported zero gaps. A self-signed certificate verifies only when it is
itself in the trust store. Measured for a `TRUSTED CERTIFICATE`-labelled
self-signed loopback leaf plus an unrelated plain root:
`createSecureContext` serves the file while every worker handshake fails
DEPTH_ZERO_SELF_SIGNED_CERT with an EMPTY stderr. The walk now refuses
to anchor on a leaf the workers do not hold, and the new gap names the
label and the remedy instead of the generic "issued by another CA"
message, which would have sent the operator after a CA that does not
exist.

BEHAVIOUR FLIP: the `no-daemon-blocks` test arm in
channel-worker-supervisor.test.ts pinned `[block without its END
line][complete block]` as a file the loader takes nothing from. That is
the divergence above recorded as truth — re-measured, the loader takes
the truncated block. The fixture is re-pointed at a `TRUSTED
CERTIFICATE` block, which the same probe shows IS the shape the arm
describes: `createSecureContext` accepts it (the daemon boots and
serves) and the loader takes nothing from it, silently.

Verification: 451 tests across pem-certificate-blocks,
channel-worker-supervisor and run-qwen-serve pass. Each of the three
fixes was mutation-verified — reverting the DER gate fails 2 tests,
dropping the BEGIN-marker termination fails 2, and dropping the
unheld-leaf check fails 1. `tsc --noEmit` on packages/cli reports the
same 6 pre-existing errors before and after, none in these files.

* fix(serve): align worker TLS validation

* fix(serve): verify channel worker TLS trust

* fix(serve): close TLS startup review gaps

* fix(serve): allow TLS channels after startup

* fix(serve): align PEM marker attempts with Node

* fix(serve): match PEM loader line semantics

* fix(serve): delegate PEM loading to Node

Close the repeated NODE_EXTRA_CA_CERTS emulation divergence by asking a short-lived child of the worker Node executable which certificates it actually loads. Keep older Node 22 releases fail-closed, inspect production source files without copying combined PEM key material, and pin the current-head buffer, EOF, NUL, BOM, and nested-label regressions.

* fix(serve): make certificate oracle fail closed

* fix(serve): fail closed on legacy CA oracle gaps

* fix(serve): preserve legacy CA loader tolerance

* fix(serve): match legacy CA byte boundaries

* fix(serve): fail closed on legacy CA inspection

* fix(serve): separate failed cert inspection from empty verdicts

* fix(serve): model worker TLS trust the way workers actually verify

* fix(serve): record NODE_TLS_REJECT_UNAUTHORIZED in the serve env guard

* fix(serve): gate loader-oracle tests on tls.getCACertificates

* fix(serve): normalize killed TLS trust probes to the generic failure code

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-25 13:43:39 +00:00
俊良
3221adc8ec fix(cli): prevent background prompt subcommand routing 2026-08-25 20:50:17 +08:00
qqqys
814b18df3c
fix(goal): cite this turn's delivered output instead of refusing over it (#9880)
* fix(goal): cite this turn's delivered output instead of refusing over it

A completion proposal had to cite every `delivered_output` from the current
turn or be refused, with guidance to read the catalog and retry. That cannot
converge. Assistant output is `delivered_output` stamped with the same turn, so
the text emitted while complying becomes another uncited entry, and the
required set grows by one on every attempt. In a reported session the loop ran
34 minutes and 8.6M tokens across 67 model calls — 32 `get_goal`, 23
`update_goal` — with the uncited list growing each round, until the user paused
the Goal by hand. Nothing bounds the retrying: a refusal ends the turn with the
Goal still active, so the runtime queues another continuation.

Nothing about that list needs the model's judgment; it is exactly the entries
the tool already computes to build the refusal. Fold them into the proposal
instead of demanding they be repeated back. The verifier still sees the
current turn's delivered output, which is what the gate was protecting, and the
model can no longer lose a race against its own narration.

The union is safe by construction: both sets are drawn from the same catalog
and are disjoint, so it cannot exceed GOAL_EVIDENCE_REFERENCE_LIMIT, which is
that catalog's own entry cap. Entries the proposal already cited are not added
twice, since a duplicated reference is itself rejected downstream. The scope is
unchanged — only a `complete` proposal is affected; a blocker still cites
whatever it chose. What was folded in is reported back as
`autoCitedCurrentDeliveredOutput` so the proposal reaching the verifier is not
a surprise.

This does not add a retry bound. That belongs with the Goal budget work, and
mixing the two would put an unrelated contract change in this diff.

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

* test(core): pin goal output auto-citation filters

* test(core): distinguish goal output citation filters

* test(core): pin empty goal output citation payload

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
2026-08-25 12:40:43 +00:00
易良
50c553550e
fix(live): restore Live Host after desktop removal (#9994)
* fix(live): restore the standalone Live Host

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

* fix(release): restore Live Host release feeds

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

* chore(skills): remove the unsupported desktop pet skill

* fix(ci): stabilize Live Host validation

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

* test(cli): complete session swap telemetry fixture

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-25 12:32:31 +00:00
qqqys
7fc6f9160c
refactor(cli): remove unused EnumSelector component (#9997)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-25 12:26:49 +00:00
qqqys
73a418ccbb
feat(goal): make get_goal's default view a summary instead of the whole catalog (#9973)
* feat(goal): make get_goal's default view a summary instead of the whole catalog

Every get_goal read returned the entire bounded evidence catalog -- up to
100 entries with 240-byte previews -- plus the Goal snapshot, which
carries the evidence checkpoint verbatim: up to 32 claims of up to 2,000
characters each, every one of which is already in the catalog as a
`goal_checkpoint` entry with the same uuid and its own preview. A Goal
that had been running for a while paid all of that on every read. In one
session the prompt grew from 44k to 277k tokens over 27 rounds; in
another the model read the Goal 32 times in 34 minutes.

get_goal now takes `view: 'summary' | 'full'`, default summary. The
summary collapses the checkpoint's claims to a count (the catalog entries
carry their previews), keeps full previews for checkpoint entries and for
this turn's entries -- the compacted proof and the records a proposal
cites next -- and caps previews from earlier turns at 80 bytes, cut on a
code point. Every uuid is present in both views and remains valid for
update_goal, which validates references against the runtime's own
catalog, never against what the model was shown. `full` returns the
payload exactly as before.

On a steady-state fixture (32 maximal claims, a 100-entry catalog, a
16-turn lineage) the read drops from 105,317 bytes to 27,225 bytes; the
test pins a 36,000-byte ceiling and a >=3x ratio on that fixture.

Mutation probes: defaulting to `full` fails 3 tests; removing either
preview exemption fails the summary test; breaking the `full` switch
fails 2; leaving the claims uncollapsed fails 2; restored suite 39/39.

* fix(goal): share one preview byte-cap and slim the summary views (#9973)
2026-08-25 12:25:58 +00:00
Shaojin Wen
5d5a2d9c31
fix(cli): graft the review anchor forward across fail-closed rounds (#9932)
* fix(cli): graft the review anchor forward across fail-closed rounds

A review round that failed to close cleanly withheld its incremental
anchor on purpose, but recovery read only the winning marker — so one
non-clean round dropped the incremental state permanently, and every
later round re-read the whole diff, with no path back on its own.

Recovery now grafts the anchor forward from the most recent earlier own
marker that carries one: the withhold is about the fail-closed round's
own range, while an earlier round's "clean up to sha" stays true, and
scoping the next round sha..HEAD re-covers exactly the gap. The graft is
own-account-only, needs a known identity, a complete work list and a
strictly earlier source round, and the rendered ledger section says
"anchoring at" with the certifying round's provenance instead of
claiming the winning round "reviewed at" it. The persisted side file
carries the graft provenance, and the chain self-check treats a grafted
anchor as usable only when its certifier matches the running model, so
the two-consecutive-withholds disclosure still fires when a cross-model
graft cannot break the loop.

Fixes #9902

* fix(cli): make grafted-anchor wording true in every state it renders (#9902)

* fix(cli): never let a grafted anchor license an upToDate stop (#9902)

* fix(cli): refuse the graft over a partial own marker the merge never counts (#9902)

A foreign winner's own-side dropped count reaches the graft's
completeness guard only through the merge branch, which an own latest
marker parsing to zero findings never enters — so a partial own marker
(version-drifted entries rejected by the admission test, or a
hand-edited list) left its dropped invisible and the graft retired
findings that are in no work list. Read the own marker's dropped
directly in that shape and refuse.

Also give the mechanism-health disclosure's onset clause the same
usability qualifier its termination clause carries: a graft that landed
but the running round cannot use does not spare the full re-read, so
the onset must not promise otherwise.

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-25 12:22:44 +00:00
Dragon
fcd003b413
refactor(web-shell): reuse canonical todo parser (#9956)
* refactor(web-shell): reuse canonical todo parser

* refactor(web-shell): reuse canonical isSubAgentToolCall (#9956)

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-25 12:19:47 +00:00
ytahdn
2367c36d6d
fix(webui): preserve hydrated goal state (#10012)
Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
2026-08-25 12:13:31 +00:00
易良
f2e593c26b
fix(core): make permissions.allow restrict the tool schemas sent to the model (#9829)
* fix(core): make permissions.allow restrict the tool schemas sent to the model (#9827)

permissions.allow only auto-approved calls; it never gated tool
registration, so the outgoing tools array kept every built-in schema
even when an allowlist was configured — contradicting the settings
docs migration table ("unlisted tools are disabled at registry
level") and breaking backends like llama.cpp that compile all tool
schemas into a single grammar.

- Activate a registry-level allowlist when settings.permissions.allow
  has at least one valid rule: built-in tools not covered by any allow
  rule are no longer registered (absent from /tools and the API
  request). MCP tools and the structured_output contract stay exempt;
  session-granted rules ("always allow", skill allowedTools) extend
  membership but never activate the allowlist mid-session.
  --allowed-tools / SDK allowedTools / legacy tools.allowed keep their
  pure auto-approval semantics.
- Complete the rule alias map so the display names shown by /tools
  (SendMessage, UpdateGoal, ...) match in allow/deny rules.

* fix(core): honor permissions.allow in the list_directory opt-in gate (#9827)

isLsToolEnabled() only read tools.listDirectory.enabled and the coreTools
allowlist, so an explicitly allowlisted list_directory passed
PermissionManager.isToolEnabled() but was never registered — absent from
/tools and the model request, with calls failing TOOL_NOT_REGISTERED. This
broke the documented tools.core -> permissions.allow migration equivalence
for exactly this tool. Consult getRegistryAllowList() with the same
coverage semantics the registry gate uses (toolMatchesRuleToolName, so
Read / ListFiles / specifier forms all count).

* fix(core): keep plan-mode lifecycle tools registered under the allowlist (#9827)

The permissions.allow registry gate covered exit_plan_mode /
enter_plan_mode / ask_user_question, so the exact reporter configuration
unregistered them. The plan-mode system reminder still instructs the model
to present its plan by calling exit_plan_mode, whose schema is then never
sent, so the sanctioned plan flow cannot complete. Exempt the three
plan-mode lifecycle tools alongside structured_output (same synthetic-
system-tool class the CORE_TOOLS docstring names; deny rules still apply).

* docs(sdk): correct allowedTools registry-allowlist contract (#9827)

The JSDoc added for QueryOptions.allowedTools (and the coreTools block)
claimed the SDK allowedTools param activates the registry allowlist and
hides unlisted built-in schemas. It does not: ProcessTransport maps it to
the CLI --allowed-tools flag, and this PR's CLI wiring builds
registryAllowList only from settings.permissions.allow. Reword both JSDoc
blocks and the two hand-maintained SDK doc pages (sdk-typescript.md,
sdk-typescript/README.md) to the shipped contract: allowedTools stays a
pure auto-approval grant; only permissions.allow in settings.json
(requires restart) activates the registry allowlist.

* docs(settings): note plan-mode lifecycle exemption in the allowlist (#9827)

The permissions.allow registry-allowlist exemption list named only MCP
tools and the structured_output contract. Add the plan-mode lifecycle
tools (exit_plan_mode / enter_plan_mode / ask_user_question) exempted in
b8ba258c40 so the documented exemption set matches the gate.

* fix(core): exempt the computer_use__* family from the registry allowlist (#9827)

* fix(core): gate command-discovered tools through the registry allowlist (#9827)

* fix(core): make registry-allowlist membership monotonic within the session (#9827)

* fix(core): narrow the skill allowedTools grant contract to restart-scoped registration (#9827)

* fix(core): count ask rules toward registry-allowlist membership (#9827)

A tool covered only by a permissions.ask rule was silently deregistered
whenever the permissions.allow registry allowlist was active: allow
["ReadFile"] + ask ["Shell"] hid the whole shell family from the model,
so the documented "always require user confirmation" silently became
"tool unavailable" and the ask rule could never fire.

Ask rules express "this tool must stay usable, with confirmation", so
they now count toward registry membership (frozen at startup for the
same restart-scoped monotonicity as allow rules).

* docs(settings): note that ask rules keep tools registered under allowlist (#9827)

* test(cli): pin registry-allowlist strip in bare mode (#9827)

The wiring tests only covered the safe-mode half of
registryAllowList: bareMode || safeMode ? undefined : ... — a mutant
dropping the bareMode guard survived the suite and would activate the
allowlist from settings while bare mode strips those same rules from
the merged allow set, leaving the bare registry's minimal toolset
ungated. Mirror the safe-mode test for --bare.

* fix(core): attribute registry-allowlist misses to permissions.allow (#9827)

An allowlist-miss rejection surfaced as "Qwen Code requires permission
to use X, but that permission was declined" citing a deny rule that
does not exist (findMatchingDenyRule finds nothing) and never
mentioning permissions.allow. When no deny rule matched and the
registry allowlist is active, emit a distinct message pointing at the
real config knob.

* test(core): pin resolveToolName coverage of every ToolNames entry (#9827)

TOOL_NAME_ALIASES hand-maintains the canonical/display-name mappings
that tool-names.ts declares; nothing enforced the sync, so a tool added
to tool-names.ts without an alias entry would compile, pass every test,
and silently never match a permission rule — the exact #9827 bug class,
now with higher stakes since a missed entry also breaks allowlist
coverage. Walk every ToolNames/ToolDisplayNames pair and assert it
round-trips through resolveToolName.

* fix(core): expose isPermissionsAllowListActive on scoped PM shims (#9827)

* fix(core): honour ask-only list_directory coverage in the opt-in gate (#9827)

* docs: align registry-allowlist contract wording across docs and JSDoc (#9827)

* docs: scope settings.md removal and whole-tool-deny claims precisely (#9827)

* fix(core): count merged allow coverage in the list_directory opt-in gate (#9827)

isLsToolEnabled() scanned only the settings-sourced getRegistryAllowList() for allow coverage while PermissionManager.isToolEnabled() counts the merged allow set (settings + --allowed-tools + SDK allowedTools + legacy tools.allowed). Under an active allowlist, list_directory covered only by a merged rule passed isToolEnabled but was never offered to registerLazy — it vanished from /tools and the model request while calls failed TOOL_NOT_REGISTERED. Count the merged allow set for coverage (activation still requires a valid settings rule) and filter empty/whitespace-only entries from activation exactly like PermissionManager.initialize's parseRules does, so a degenerate [""] entry cannot activate the gate here while the permission system reports the allowlist inactive.

* test(core): pin activation source and merged-allow coverage of the list_directory gate (#9827)

Every existing isLsToolEnabled test fed the identical array as both allow and registryAllowList, so the settings-only vs merged-allow distinction was unpinned and the R4-1 divergence shipped uncovered. Add three cases shaped like the CLI wiring: coverage by a merged (non-settings) allow rule under an active allowlist registers the tool; merged-only coverage with no settings rule does not activate the allowlist; an empty settings entry ([""]) does not activate it either.

* fix(core): attribute scheduler denials to permissions.allow only for uncovered tools (#9827)

The allowlist-miss message fired for any disabled tool with no matching deny rule while the allowlist is active — including tools rejected by the legacy coreTools gate that ARE covered by an allow rule, where 'not covered by any permissions.allow rule' is wrong and the remediation a no-op. Expose isCoveredByAllowOrAskRule on PermissionManager and take the allowlist branch only when the tool is genuinely uncovered; covered tools fall back to the generic declined message. The optional call keeps scoped PermissionManager shims (installed via 'as unknown as PermissionManager') from throwing until they grow the delegation.

* test(core): pin the covered-tool fallback for scheduler denial messages (#9827)

Add a scheduler-level case where the allowlist is active, no deny rule matches, and the disabled tool IS covered by an allow rule (the legacy coreTools gate shape): the message must be the generic declined one, not the permissions.allow attribution. Also make the existing allowlist-miss stub explicit about coverage.

* fix(core): register request_shutdown in the permission rule alias map (#9827)

Merging origin/main brought ToolNames.REQUEST_SHUTDOWN (#9806) but no TOOL_NAME_ALIASES entry, which the resolveToolName exhaustiveness test added on this branch pins. Map request_shutdown / RequestShutdown so permission rules can address the tool.

* fix(core): guard the list_directory allowlist gate against non-string rules (#9827)

isLsToolEnabled()'s activation check and coverage scan called raw.trim() / parseRule(raw) directly while PermissionManager.initialize computes the same thing through parseRules, whose r && r.trim() filter skips falsy entries. Settings load performs no element-type validation (the schema declares only type: array), so a stray null in settings.permissions.allow/ask — or in the legacy tools.allowed key riding the merged coverage set — threw TypeError during createToolRegistry and crashed startup while PermissionManager.initialize tolerated the same settings file. Mirror the parseRules guard with a typeof check in both the activation check and the coverage predicate, and pin both arms (tolerated entries still activate/cover, a [null]-only list keeps the gate closed).

* fix(core): exempt task_stop from the permissions.allow registry gate (#9827)

task_stop satisfies the PR's own two written exemption criteria but was missing from the set: it is shouldDefer=true (task-stop.ts), the exact deferred-schema property the computer_use__* exemption cites, and it is advertised to the model by a registered tool's copy — run_shell_command's schema says to use task_stop to stop a background command (and not to use broad process-name kills), and the background-promotion result instructs task_stop({ task_id }) verbatim. Under the reporter configuration the suite pins, run_shell_command stays listed while task_stop was gated out, so the sanctioned stop flow failed. Add the exemption and pin it next to the plan-mode exemption tests, including that a whole-tool deny rule still wins via the existing evaluate pass.

* fix(core): keep shim denials on the pre-#9827 message when coverage is unknown (#9827)

The optional isCoveredByAllowOrAskRule call's : true fallback routed shim-mediated rejections of COVERED tools into the new allowlist-attribution message, contradicting the comment above it ('they keep the pre-#9827 message meanwhile'). Both production shims (memory-scoped-agent-config.ts, skillReviewAgentPlanner.ts) Pick a partial interface without isCoveredByAllowOrAskRule, so for them the ternary always took the allowlist arm — telling the user a covered tool 'is not covered by any permissions.allow rule' when a different gate (e.g. the legacy coreTools allowlist) rejected it. Flip the fallback to false so unknown coverage stays on the pre-#9827 declined message, and update the shim test to pin that message instead of the allowlist one.

* test(core): pin that ask-only rules never activate the allowlist (#9827)

The suite pins ask rules counting toward allowlist membership, but nothing pins the complementary activation boundary: no test constructed a PermissionManager with only permissionsAsk (no permissionsAllow) and asserted the allowlist stays inactive. Current behavior is correct; this guards against a future edit folding ask rules into activation, which would turn an ask-only posture (permissions.ask: ["Shell"], no allow rules — a natural 'always confirm shell' config) into an active allowlist that deregisters every unlisted built-in. The nearest existing test ('no allow rules → allowlist inactive') uses no rules at all and would still pass.

* fix(core): exempt tool_search from the permissions.allow registry gate (#9827)

Under a narrow active allowlist, tool_search itself was gated out of the
registry. Without ToolSearch, client.ts resolveDeferredToolsForReminder
eagerly force-reveals every registered deferred tool (all mcp__* and the
deferred computer_use__* family) into the eager model request, and
preloadDeferredToolsWithinBudget early-returns — inverting the
schema-shrink goal into maximal schema bloat for exactly the deferred
families the other exemptions preserve for ToolSearch discoverability.
Pre-#9827 tool_search always bypassed the legacy coreTools gate as a
non-core tool.

* test(core): pin the deny-rule arm's precedence in the scheduler permission message (#9827)

The three-way message branch in CoreToolScheduler covers the allowlist-miss
arm and the generic fallback arm, but every findMatchingDenyRule mock
returned undefined, so the deny-rule arm — whose position FIRST in the
if/else-if chain is what makes a real denial cite the matching rule instead
of the allowlist attribution — had no scheduler-level coverage. Add two
tests where findMatchingDenyRule returns a matching rule: one with the
allowlist arm armed (active allowlist + uncovered tool) pinning the
if/else-if ordering, one without an active allowlist pinning the deny arm
over the generic declined fallback. Mutation-checked: disabling the deny
arm fails both tests.

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

* test(core): pin deny/ask sibling semantics at the discovery gate (#9827)

The discovery-gate test built its PermissionManager with EMPTY ask/deny
lists, so the gate's two documented sibling semantics were unpinned:
settings.md says a whole-tool deny rule removes a discovered tool from
the registry even under an active allowlist, and an ask rule keeps a
discovered tool registered ("always require confirmation" must never
silently become "tool unavailable"). Add two discovery-gate tests with
deny-covered and ask-covered PermissionManager configurations: the denied
tool is also allow-covered so only the deny branch of isToolEnabled can
reject it, and the ask test carries an uncovered control tool proving the
gate is active in the same run. Mutation-checked: ignoring deny decisions
fails the deny test only; dropping ask coverage from
isCoveredByAllowOrAskRule fails the ask test only.

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

* docs: match the allowlist activation wording to the real predicate (#9827)

Four surfaces said the permissions.allow registry allowlist activates
"when at least one allow rule is configured", but
PermissionManager.initialize computes activation as at least one VALID
rule from settings.permissions.allow only (getRegistryAllowList): a
malformed entry never activates it, and auto-approval-only sources such
as the --allowed-tools CLI flag / the SDK allowedTools parameter never
do either. Reword settings.md, the SDK docs, the sdk-typescript README
and the coreTools JSDoc to the exact predicate, and complete their
exemption lists with task_stop and tool_search, which isToolEnabled
exempts but the docs did not name. Docs-only.

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

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-25 11:57:24 +00:00
Shaojin Wen
519e824dc7
fix(ci): give the macOS and Windows lanes a trigger again (#9370)
* fix(ci): give the macOS and Windows lanes a trigger again

Both lanes are gated on `merge_group`, and no merge queue is enabled on
this repository — the `main` ruleset carries only deletion,
non-fast-forward and pull_request rules, no status check is required,
and merges land as squashes. The last `merge_group` run of anything was
2026-07-02. So the gate was an off switch: the lanes reported as
"skipped" on every pull request, which reads as agreement, and nothing
ever reached them afterwards. The only signal this repository has about
a host that is not Linux with a GNU userland had been silently off for
six weeks, which is how #9220 shipped a GNU-only `realpath -m` in a
workflow guard with the suite that pinned it red on every Mac.

Three triggers now, in cost order.

A pull request whose diff a new classifier recognises as
platform-sensitive: shell scripts of every dialect, workflow and
composite-action YAML and the scripts they call, the script layer and
its tests, the test-runner configuration that decides which suites run
where, the root manifests, and source paths whose segments name a
platform-coupled subsystem. It is a net, not a proof — it cannot see a
platform assumption inside an ordinary source file, and no path rule
ever will — so every unknown answers "sensitive": an unreadable listing,
an unparsable entry, a fork pull request, a truncated file list, or the
classify job failing outright all end as "run the lanes". Only a
confident `false` skips them.

The merge queue, if it is ever enabled again, unchanged.

And a nightly run on `main` for everything the path list cannot see.
Every other job in the workflow excludes `schedule` explicitly, so a
nightly is exactly two jobs, and 'Qwen Code CI' joins the workflows the
main-failure watcher opens autofix issues for — a red lane nobody is
told about is the same silence the queue gate produced. That watcher
gains a trigger-level `branches: ['main']` filter so the CI workflow's
pull-request completions do not raise an event there just to skip.

The classifier runs in its own small hosted job rather than as a step in
`classify_pr`: that job's outputs pick the Linux runner for the whole
run, and this one needs a checkout — on a pool whose workspace other
jobs have poisoned before. It checks out the pull request's BASE commit,
never the head: it runs before any review and executes a script from the
tree it checks out. Its listing goes through the existing
classify-pr-profile.sh wrapper, extended with a mode argument, because
that wrapper's whole point is that one PR is never listed twice and
classified differently in two places.

Twelve tests pin the wiring — the triggers, the fail-safe direction of
the gate, the base-commit checkout, that a nightly stays two jobs, and
the alerting — and ten more pin the classifier itself, including the
substring traps (`Shellfish.tsx`, `cryptic.ts`, `plateauDetector.ts`
must not drag both lanes in) and every fail-safe path. Mutation-checked:
flipping the gate to `== 'true'`, dropping the schedule from a lane,
dropping the nightly guard from the ubuntu job, pointing the checkout at
the head, dropping the classifier's test from the helper list, and
removing the schedule trigger are each caught.

* fix(ci): gate the Windows lane's checkout verification per trigger

The first thing the revived triggers hit was not a test failure but the
lane's own plumbing. `test_windows` verifies its checkout with
`verify-checkout-head`, and the input was written when this lane ran in
the merge queue alone: `expected_sha: github.event.merge_group.head_sha`,
with no event gate. On a pull request that expression is empty, the
action refuses an empty SHA, and the lane went red in 63 seconds without
running a test — the first Windows run in six weeks, failing on the
trigger rather than on the code.

Give it the event-aware shape the Ubuntu gate already uses, and skip it
where there is nothing to verify: the scheduled and dispatch runs check
out a branch by name, not a head commit.

Pinned generally rather than by name: for both lanes, any step whose
inputs read a `github.event.<event>` context must be gated to that
event, in the step's own `if` or in the expression itself. Restoring the
old spelling turns that test red.

* fix(ci): close review round on the revived platform lanes (#9370)

* fix(ci): pin the Windows lane routing to the canonical trust policy (#9370)

* fix(ci): host-probe the macOS lane's bash and GNU dependencies (#9370)

The revived macOS lane ran the shared suite for the first time since the
merge queue went dark and failed on exactly three tests, identical across
three consecutive runs: the bite-check block and the baseline A/B green
path die with `mapfile: command not found` (a bash >= 4.4 builtin; macOS
ships 3.2), and the health-probe repair case trips over BSD `mktemp -u`
attempting to create where GNU's print-only `-u` just names the canary.

Both suites pin scripts that only ever execute on Linux runners —
ubuntu-latest or the Linux ECS pool — so the defects cannot exist in
production; the suites just cannot run those scripts on a macOS host.
Follow the convention #9220's fix established for the realpath case in
the same file: probe the host capability, not the platform, and skipIf
the three dependent tests where it is absent. A Mac with a newer bash or
GNU coreutils fronting PATH keeps the coverage; the Linux lane runs all
three unconditionally. The large bite-check test body is re-indented by
prettier around the new skipIf wrapper; no other token changed.

* fix(ci): stop the subsystem rule matching compounds that name something else

The platform-sensitivity classifier split a path segment on dashes and
underscores anywhere, so `packages/web-shell/**` matched the `shell`
keyword — one of this repository's largest packages, a browser UI with
no host coupling, summoning both expensive lanes on every change to it.
That is the cost the gate exists to avoid, spent on the wrong diffs.

A keyword now counts when it NAMES the thing: a whole path segment
(`src/sandbox/**`, `platform/paths.ts`, `shell.ts`) or the head of a
hyphen/underscore stem (`pty-host.ts`). Not a trailing part of a
compound, which belongs to whatever the leading word names, and still
not a substring inside a longer word.

Pinned both directions, including a directory that IS named for the
subsystem wherever it sits (`web-shell/components/shell/**` stays
sensitive). Mutation-checked: dropping either rule, or restoring the
split-anywhere spelling, turns the suite red.

Also drops a wrong issue citation in the watcher test's comment: the
nightly comes from this change, not from the wipe-guard back-port.

* test(ci): pin the gate as a disjunction and the watcher's name binding

Two mutation-survivable gaps in this PR's own tests.

The clause-presence assertions left a connective mutation alive: `||` →
`&&` between two event clauses keeps every asserted string in place and
makes the gate unsatisfiable for every trigger — both lanes silently off
again, which is the state this PR exists to end. Read the event group and
require it to be a disjunction, allowing `&&` only inside the
pull-request clause that binds to the classifier output.

And the watcher's binding to this workflow is by display name:
`workflow_run.workflows` matches the watched workflow's `name:`, so
renaming ci.yml unhooks the nightly's alerting silently. Pin both sides.

Both checked by mutation: flipping one `||` and renaming the workflow
each turn a named test red.

* fix(ci): close the classifier's CRLF gap and widen the lane step scan

Three findings from this round, all in the direction of the tests and
the parser being less clever than they claimed.

The classifier's JSONL reader split on `\n` while its sibling splits on
`/\r?\n/`. Every suffix rule here is end-anchored, so one carriage
return on a CRLF listing would leave `build.sh\r` and classify a
script-layer change as ordinary source. Matched to the sibling, with a
fixture on both the JSON and the raw-line path.

The per-trigger step scan serialized only `with:` inputs, so the same
defect wearing an `env:` or `run:` key escaped it; it now reads all
three.

And the nightly blast-radius guard tested for the MENTION of an
allowlisted event rather than the IMPOSSIBILITY of `schedule` — a job
gated `pull_request || schedule` satisfied it while running every night.
It now requires the absence of an explicit schedule clause too.

* style(ci): format the runner-routing suite

The Windows trust-policy matrix added last round left the file outside
prettier's style, which the repository's lint step fails on; main's copy
is clean. Formatting only — the nine assertions are unchanged and still
pass.

* fix(ci): gate the two mapfile-crossing gate tests on the host probe (#9370)

* fix(ci): keep pull requests off the persistent Windows pool and cover native audio (#9370)

A pull_request run executes the workflow YAML from the PR's own merge
commit, so the test_windows runs-on trust clause it evaluated could be
rewritten by any PR the lane admits. Every pull request now runs on
hosted windows-2022 unconditionally; the pool is reached only by the
post-approval merge queue, schedule and dispatch, guarded by the
kill-switch. The routing tests and the exact-line pin are re-pointed at
that enforceable shape.

Also add audio to the platform-sensitivity classifier's subsystem
keywords: packages/audio-capture is a node-gyp workspace compiled
per-host on exactly the two revived lanes, but its native sources
(.cc/.mm/.gyp) carried no rule and a PR touching only them skipped both
lanes. The workspace directory now classifies sensitive; an ordinary
.cc elsewhere stays ordinary source.

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

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-25 11:53:05 +00:00