100 Commits
Author SHA1 Message Date
易良andQwen-Coder a32684604a fix(core): surface Anthropic refusal details (#16079)
* fix(core): surface Anthropic refusal details

Propagate Anthropic refusal stop_details through candidate finishMessage and the Finished event so both terminal UIs can show the provider's refusal category instead of a generic empty safety stop.

Co-authored-by: Qwen-Coder <[email protected]>

* style(core): apply Prettier to the refusal-detail diff

The incremental lint gate selected 12 changed files and Prettier rejected
four of them; this commit only re-wraps those lines, no behaviour change.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvygjkzk7s

* fix(core,cli): scope the refusal notice to safety stops

Review follow-up on #16079:

- Both renderers now prefer `finishMessage` only for `SAFETY` stops. The
  field is a provider wire field (`@google/genai` types it), so a Gemini turn
  carrying it could otherwise replace the curated notice for `MAX_TOKENS` and
  even add an info line on `STOP`.
- The refusal category lookup uses an own-property check: a provider `category`
  of `__proto__` / `constructor` now falls back to the generic notice instead
  of returning an object that renders as `[object Object]`.
- `llm-chat` deletes `finishMessage` together with the deferred
  `finishReason`, so the stream no longer yields a notice with no reason.
- Tests: category table (all mapped categories + prototype key + unknown) and
  a non-safety renderer case pinning the curated notice.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw651acyim

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-07 23:42:24 +08:00
易良andyiliang114 708ba347d3 feat(ci): add disposable external PR runners (#16513)
* feat(ci): add disposable external PR runners

* test(ci): align platform lanes with disposable runner routing

---------

Co-authored-by: yiliang114 <[email protected]>
2026-11-07 14:24:33 +00:00
1762efe4cb fix(channels): keep a delivered turn completed when the notice send fails (#16512)
* fix(channels): keep a delivered turn completed when the notice send fails

The stacked-skill turn notice is delivered after the answer, inside the
same try block that guards the delivery. A failing notice send (for
example a QQ 429 RATE_LIMITED DeliveryError) bubbled into the catch that
emits failed(phase: 'delivery') and rethrows, rewriting an already
delivered turn into a failure even though the user received the answer.

Guard the notice send and log the failure to stderr instead, matching
the deliveryStarted invariant: once delivery started, the turn's
outcome is fixed.

Refs #16441

* fix(channels): fail the turn when the notice was its only delivery

The prompt result is `turnNotice || body`, so with an empty body the notice
send is the turn's only delivery. Swallowing its failure reported the turn
completed while nothing reached the user; rethrow so the outer catch emits
failed:delivery, as main did. Pin the notice-only branch and the log text.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw5ylt0cid

* fix(channels): log which notice was dropped when its send fails

R2-1: the catch that keeps the turn `completed` after a failed notice send
logged only the transport error. The notice text had already been sliced out
of the answer, so the log line was the last place that knew what the user
never received. Include it bounded and sanitized, matching the file's logging
convention, and pin it in the existing regression test.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw651acyim

---------

Co-authored-by: zzj <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-07 10:40:37 +00:00
4c5820c745 fix(web-shell): close out managed-host review threads from #15928 (#16203)
* fix(web-shell): close out managed-host review threads

#15928 merged with 19 review threads left open. This lands the code fixes and
the tests that actually pin them.

Code:
- shadowDom: the ref-counted release is now idempotent, so calling one
  install's cleanup twice no longer strips a sibling shell's stylesheet
  (R2-4).
- ManagedArchivedSessions: the host status callback is held in a ref, so a
  host that stores the status in state no longer re-emits on every render and
  loops (R2-7).
- ManagedSessionsPage: the pane measurement re-runs when the host supplies a
  session id after mount, so a host that starts in the waiting state still
  gets a ResizeObserver and the inline task panel (R1-2).
- ManagedSessionsPage: the pending-owner link goes through openSession, so it
  clears the draft/error of the conversation being left and forwards the
  owner's workspace id (R1-13).
- ManagedAgentWebShell: `baseUrl` is optional when `managedAgentProvider` is
  injected, and a missing one fails loudly instead of resolving requests
  against `<origin>/undefined` (R1-11).

Tests:
- shadowDom: a double release keeps the sibling sheet (R2-4).
- ManagedArchivedSessions: a fresh inline callback settles (R2-7).
- ManagedSessionsPage: pane measurement after a late session id (R1-2),
  pending-owner navigation carries the workspace (R1-13), the `:host:` pending
  key keeps sessions apart (R1-4), the empty-state test uses the real failure
  shape and asserts banner copy instead of a bare truthiness flag (R1-5), the
  welcome block is asserted absent next to a failure banner (R1-8), and a
  foreign error banner is dropped on session switch (R1-9).
- ManagedAgentWebShell: no provider and no baseUrl fails loudly (R1-11).
- managed-host.dom: the pending row assertion matches the row's actual copy
  instead of a substring it no longer contains (R1-7).

Every new assertion was verified red with the fix removed.

* fix(web-shell): keep the pending owner's workspace and late status callbacks

Three review follow-ups from the #15928 threads:

- The pending-owner link resolved the owner's workspace from the served
  session page, which is filtered, paginated and empty in host mode. When
  the owner was absent the click forwarded no workspace and the host's
  handler then cleared the selection. Capture the workspace id on the
  pending record at submit time and prefer it, keeping the page lookup as
  the fallback for records written before this change.
- The `baseUrl` guard tested truthiness, so `baseUrl=""` — a supported
  same-origin value that resolves to the page origin — threw during
  render. Only a missing prop is a configuration error now.
- Holding the archived-status callback in a ref keeps the loop closed but
  also dropped delivery to a host that supplies the callback only after
  the list settled. Re-emit on the absent -> present transition while
  keeping identity churn from re-running the effect.

Each case has a regression test that fails without its fix.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw0nuit6aw

* style(web-shell): apply prettier to ManagedArchivedSessions test

The Lint & Static check fails on prettier formatting of the late-status
callback's parameter type annotation. Formatting-only change; eslint and
prettier now pass on the file.

* fix(web-shell): close out managed-host R2 review threads

- reject a null baseUrl, not only an absent one: a plain-JS host erases the
  prop type, so an unresolved config value arrives as `null` and every
  request resolved against `<origin>/null`
- state the requirement in the README and both design-doc languages, which
  still promised that omitting the provider preserves Java behavior
- pin the pending-record write half (the submitted workspace) and the
  readPending rejection of a non-string workspaceId; both tests fail when
  the guarded line is removed

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw0s4ubab2

* fix(web-shell): render the shell fallback for a missing baseUrl and pin banner clearing

The `baseUrl` guard throws from the shell's own render body, above the
ErrorBoundary it returns, so a host mounting before its config resolves saw an
empty container instead of RootErrorFallback. The provider now resolves one
level below an outer boundary, and the pending-owner navigation test also pins
the `setError(undefined)` half of openSession.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw4plfohgl

* fix(web-shell): name the shell fallback and assert the contained baseUrl failure

The outer config boundary returns an unnamed arrow, which `react/display-name`
rejects, so pin its name. The two baseUrl-guard tests still asserted the
pre-boundary contract (the guard's throw escaping to the host); the boundary now
reports that failure through the shell's own fallback, so assert both halves:
the fallback renders and the guard's message still reaches `console.error`.

The pending-owner test's added banner pin is reverted: the navigation error
renders only when `interactive` is false, so that path cannot show it.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw4rqlfigo

* style(web-shell): apply prettier to ManagedAgentWebShell

The Lint & Static lane failed at "node scripts/lint.js --prettier" on
packages/web-shell/client/ManagedAgentWebShell.tsx: the props interface
`extends` clause and the RootErrorFallback JSX needed reflowing.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw52gecrh3

* docs(web-shell): describe the baseUrl guard's observable behavior

The guard runs below the shell's own error boundary, so a host sees the
fallback render and reads the cause from console.error -- never a throw.
Correct the three copies of that contract and cover the resetKeys recovery
path (missing baseUrl, then a present one) with a transition test.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw56qpv9h9

* test(web-shell): pin same-origin storage key and the provider reset path

R5-1: assert the constructed provider's storageKey resolves against the page
origin, so coercing `baseUrl` to undefined can no longer keep the test green
while requests resolve against `<origin>/undefined`.
R5-2: add a provider-only re-render case so the boundary's provider reset key is
pinned, not just the `baseUrl` half.
R5-3: document that recovery needs a re-supplied `baseUrl` or provider, because
the boundary clears only through its reset keys.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw5q15wki1

---------

Co-authored-by: root <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-11-07 17:45:38 +08:00
易良andQwen-Coder 5faa620af9 fix(core): keep the user's instruction last in the model request (#16274)
* fix(core): keep the user's instruction last in the model request

A fresh turn's user entry is assembled from independent sources: per-turn
reminders ahead of the prompt, then hook/Mod context and the memory catalog
behind it. A long multi-part instruction therefore lands mid-entry with tens
of kilobytes of injected context on both sides, and a flash-class model
answered that it had received no task while the instruction sat there.

Reorder the request copy at the single point where every main-turn request is
assembled, moving the injected run that trails the caller's own text ahead of
it. Stored history keeps its shape: the transcript display projection and the
resume receipt both read the hook block as the final part, so reordering the
stored entry would break the projection and re-deliver hook context every turn.

Leading tool results keep their position, a non-leading tool result leaves the
entry untouched, and the volatile reattach tail stays last so the provider
cache breakpoint keeps its anchor. A reminder the caller placed between two of
its own parts bounds the movable run and stays put.

Refs #16259

Co-authored-by: Qwen-Coder <[email protected]>

* docs(core): correct the prompt-context placement prose

The Constraints bullet and the isInjectedPromptContext docstring both claimed a
caller-authored part shaped like a reminder keeps its own order. The rebuild
classifies by shape across the whole entry, so that promise does not hold for
the caller's last part: a trailing part in either envelope is treated as
injected and moves ahead of the caller's text. Describe the mechanism instead,
in the docstring and both design docs.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw1upqc7cl

* fix(core): reorder injected context on every user entry

The send boundary reordered only the pending entry, so request N sent
[reminder, hook, catalog, ...prompt] while request N+1 rebuilt the same
entry from stored history as [reminder, ...prompt, hook, catalog]. The
shared provider prefix then ended at the leading reminder and every
injected block was re-prefilled at full input price on every turn - the
failure mode REATTACH_BOUNDARY_METADATA exists to prevent (#11627).

Applying the same rule to every user entry in the request copy keeps an
entry's wire shape position-independent, so request N+1 extends request
N's prefix exactly. Stored history is still untouched, which keeps the
transcript projection and the resume receipt intact.

Pin it with a two-turn test that asserts the first turn's entry is
re-sent byte-identically, and compare each request with the previous one
instead of comparing the request prefix with stored history.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw25fj8od0

* docs(core): state the reorder's every-entry scope and name the third updated assertion

The every-entry pass landed in 96e6b0c but two surfaces still described the
older pending-turn-only scope: the send-boundary comment and the
`movePromptContextAheadOfUserText` docstring opening. Both now say what the
code does, without narrowing the scope that keeps request N a prefix of
request N+1.

The design pair's validation section claimed two assertions were updated; the
diff touches three. Name the multi-turn prefix check, record why it compares
request N against request N+1 instead of against stored history, and note that
injected-part presence is still guarded by the other checks plus the test's
own snapshot count.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw29pur5d6

* fix(core): keep the compaction cache prefix aligned with the reordered request

The main request reorders every user entry so injected context precedes the
caller's text, but the cache-sharing compaction side-query still shipped the
stored order. The provider's longest matching prefix then ended at the
leading reminder, so every auto-compaction re-prefilled the rest of the
history at full input price.

Move the reorder helper out of llm-chat.ts and apply it to a copy of the
shared payload. The cold path and the post-compact image restore keep
reading the stored order.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw2e06dfdc

* test(core): pin prompt-context-order with a collocated test file

The helper shipped without a test of its own, so the `lastOwnIndex === -1`
half of the early return was unpinned: an entry whose parts are all tool
results (`nonInteractiveCli` produces one for parallel calls) would be
rewritten into a duplicate `tool_result` under the same call id if the
guard were dropped. Cover the guard, the all-injected entry, the mid-entry
tool result, the caller's interleaved reminder, the reattach tail, and
non-mutation.

The design doc also claimed the pass lives at the single point where every
main-turn request is assembled. The compaction side-query is that point's
one exception: it builds its `promptCacheSharing` payload from
`getHistoryShallow(true)` and reorders its own copy, while the cold path and
the post-compact image restore keep the stored order. Say so, and list the
compression test and the new helper test under Validation.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw3xqcjtfh

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-07 16:43:37 +08:00
易良andQwen-Coder 5890329390 fix(ci): probe the host egress proxy so mainland release hosts can build the sandbox image (#16281)
The docker-container driver's buildkitd inherits neither the runner's proxy env nor the host network namespace. On ecs-qwen-hz1-17/18 the DNS answer for registry-1.docker.io is poisoned, so the pinned node:22-slim resolution dies with a direct-dial timeout before any application step — five consecutive image-build failures (#16039, #16162) that forced ECS_RELEASE_HOST_RUNNER_ENABLED=false.

Probe the fleet-managed proxy marker and inject network=host plus env.*_PROXY driver opts only when the proxy answers a live registry probe. Hosted and HK runners have no marker file, so the probe emits nothing and their builds keep the default direct route. Verified live on hz1: unopted builder reproduces the incident, opted builder resolves the pinned digest for both platforms.

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-07 08:31:15 +00:00
易良andQwen-Coder f087e699af fix(core): register promoted initial commits in unborn repos (#16081)
* fix(core): register promoted initial commits in unborn repos

A promoted command captured its pre-spawn HEAD as null both when the repository was unborn and when the probe failed. The registration path then skipped both cases, so amending the agent's own initial commit was blocked.\n\nCapture the pre-spawn state as either a SHA or an explicitly unborn HEAD. The promoted lineage check now requires a null predecessor only for the unborn case, while repository or probe failures still skip registration. Add a real-git regression for a promoted initial commit.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(core): fail closed when the pre-spawn HEAD probe is terminated

Only a genuine git non-zero answer (status 128, no signal) proceeds to the
repository probe. A killed, timed-out, or failed-to-spawn HEAD lookup now
returns null instead of asserting a verified unborn repository, which the
promoted path could otherwise pair with a single-entry reflog to register a
pre-existing root commit the agent never made.

Co-authored-by: Qwen-Coder <[email protected]>

* style(core): prettier formatting for probe regression test

Co-authored-by: Qwen-Coder <[email protected]>

* docs(core): scope the lineage guard's fail-closed claim to born HEAD

R1-1: the docblock stated "a foreign commit never earns an exemption" as a
fail-closed property, which the unborn arm makes false — `null` is a sentinel
every root commit satisfies, so a root commit somebody else lands during the
promoted window is indistinguishable from this command's own. Say that, and
scope the five-star guarantee to a born HEAD. The behaviour is the trade
tracked on #12523; only the written invariant was wrong.

R1-2: pin that the PATH `git` shim is live before relying on it. Both closing
assertions also hold when the shim is bypassed (the lineage check then rejects
on a non-null predecessor), so the row could green without exercising the
probe-termination path it exists to pin. Verified by mutation: changing the
shim's match from `--verify` to `--bypass` turns this row red.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw1dkg4zbx

* docs(core): scope the promoted-lineage claim to what the predecessor proves

The `requirePredecessor` paragraph said "a foreign commit never earns an
exemption", and the promoted call site said the guard keeps the registered
SHA one this command created. The lineage check only proves the newest
reflog `commit:` entry moved HEAD from `preHead`; a foreign commit landed
directly on `preHead` while this command's own commit never landed has that
same predecessor and still registers, so both statements overclaimed.

Scope them: born-HEAD narrowing is not closing, and the fail-closed
guarantee holds only once this command's own commit landed first.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw2g5c3hdf

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-07 14:44:43 +08:00
易良andQwen-Coder 2b94314b49 fix(cli): scrub advisor selector from startup errors (#16083)
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-07 13:42:09 +08:00
bbff05f035 fix(serve): treat a payload-invalid start reply as a definite refusal (#16460)
`HostedWorkspaceBroker.execute()` only treated the two 409 conflict codes
as a definite "never started" refusal. When the Runtime Broker rejected a
deferred tool payload it could not encode (an unpaired surrogate) it
answered `:start` with HTTP 400 `runtime_payload_invalid`, which the
client read as a lost reply: it polled the observation endpoint until
`observationMs` (120s, up to 180s) and then surfaced the session as
Hosted recovery-blocked with the workspace lease still held.

A payload the Broker refused to parse never became an invocation, so no
poll can turn it into a reply. Rethrow the 400 `runtime_payload_invalid`
rejection from the `:start` catch alongside the 409 conflicts; the
polling path and the unknown-execution reconciliation are unchanged.

The new test drives a fixture Broker that answers `:start` with that 400
and asserts the rejection propagates with only the `:start` request
observed; it fails before this change because the extra observation GET
appears in the request path list.


Patrol-Run: qwen-issue-patrol/jmw4xgcu3zh

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-07 12:50:30 +08:00
3d9988abc2 fix(cli): surface stacked-skill overflow in non-interactive and ACP runs (#16404)
* fix(cli): surface stacked-skill overflow in non-interactive and ACP runs

`parseStackedSlashCommands` already reports `exceededMax`, and the two
interactive surfaces warn about it. The non-interactive path
(`handleSlashCommand`) ignored the flag, so headless (`-p`, JSON,
stream-JSON) and daemon/ACP runs silently dropped the extra `/skill`
tokens with no signal at all.

- carry the flag out of the stacked branch as `stackedSkillOverflow` on
  the `submit_prompt` result, using the same copy as the interactive
  surfaces;
- headless writes it to stderr, so it never becomes a `message` of type
  `error` and the run keeps its exit code;
- ACP emits it as a client-visible slash-command notice before the
  prompt runs.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-issue-patrol/jmw3ofzhmya

* fix(channels): carry the stacked-skill overflow notice with the turn reply

Channel bridges deliver only prompt()'s resolved text, so the overflow notice
emitted on the slash-command channel was shadowed as soon as the turn produced
model text and channel users never saw the warning. Tag the notice in `_meta`
and route it into the turn's text in both bridges.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw3tg0yrfb

* fix(channels): keep the turn notice when a boundary clears the chunks

The notice was pushed into `chunks`, which `clearChunks` empties on every
`responseBoundary`, and the `per_task` branch never read it at all, so the
stacked-skill warning still disappeared on any turn that made a tool call.

Give the notice its own buffer in both bridges that boundaries never clear,
and compose it into every return path (turn text and `per_task` result). The
slash-command slot keeps its existing precedence and the pinned
response-boundary tests stay unchanged.

Tests: tool-call-boundary and `per_task` arms for both bridges, plus a
Session-side test that the producer tags the overflow notice with
`turnNotice: true`; each new arm reds when the buffer is removed.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw3zvicvfk

* test(channels): pin the turn notice on the real boundary and empty replies

Address the follow-up review on the turn-notice carry:

- R3-1: the daemon boundary test fired `responseBoundary` before the queued
  notice was consumed, so it cleared nothing; drive the boundary from an
  in-turn `tool_call` instead and assert it goes red when `clearChunks`
  regains `turnNotice = ''`.
- R3-3: the `turnNotice` wire key was a bare literal on every side; export
  `CHANNEL_TURN_NOTICE_META_KEY` beside the sibling keys and use it in the
  producer and both bridges.
- R3-4: a notice-only turn returned `"<notice>\n\n"`; drop the separator when
  the body is empty and cover the exact resolved string.
- R3-5: cover the per-session filter in both bridges so another session's
  notice cannot prefix this turn's reply.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw445tvbfr

* fix(channels): deliver skill overflow notices outside response buffers

---------

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: zzj <[email protected]>
2026-11-07 10:49:43 +08:00
e3cbea0096 fix(cli): surface workflow agent approvals in the interactive TUI (#16425) (#16432)
* fix(cli): surface workflow agent approvals in the interactive TUI (#16425)

A backgrounded workflow agent that hit a tool-approval gate deadlocked in
the interactive TUI: the registry parked the approval, the footer showed a
"needs approval" badge, but there was no way to answer it, so the run never
made progress. Two independent defects caused it.

1. `WorkflowRunRegistry.parkApproval` treated the display-only
   `approvalChangeCallback` as a sufficient host channel. A host that could
   only repaint the badge therefore accepted the approval and then had
   nothing to answer it with. It now fails closed unless a real
   `approvalRequestCallback` responder is registered, so a badge-only host
   rejects the park and the caller sees a cancelled request instead of a
   silent forever-wait.

2. `useBackgroundTaskView` never registered a responder at all, so even a
   host that wanted to answer had no channel. It now registers its snapshot
   refresh as the approval-request responder (and clears it on unmount),
   which makes the parked approval part of `entries` — the surface the
   background-tasks dialog renders and settles through
   `resolvePendingApproval`.

`BackgroundTaskViewProvider` also promotes a parked workflow approval to
the detail view once per `approvalId`, because the approval can only be
answered from there; keying on the id keeps a user's "close" from bouncing
straight back into the dialog on the next refresh.

Scope: interactive TUI workflow approval path only. Headless and ACP hosts
are untouched.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-issue-patrol/jmw4ig8e5z4

* fix(core): drop the redundant responder guard after the park gate

TS narrows approvalRequestCallback to non-undefined once the park gate
requires it, so the extra truthiness check is dead and fails tsc (TS2774).

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-issue-patrol/jmw4ig8e5z4

* fix(cli): type the workflow approval fixture and format the new tests

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-issue-patrol/jmw4ig8e5z4

* test(core): register a real approval responder in the orchestrator bridge test

The narrowed park gate rejects an approval when the registry has no
approvalRequestCallback, so a badge-only host (change callback only) leaves
pendingApprovals empty and the bridge test times out. Register the responder
the production host also provides, matching the gate's contract.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw4lb42xgf

* fix(cli): surface each parked approval once, in list mode (#16432 R1-1, R1-2)

The single-id latch keyed on the first pending entry's head approval, so a
second approval parked behind a still-parked head never surfaced. Latch a
set of approvalIds and select the entry holding an unsurfaced one.

Auto-open also landed in detail mode, which arms the tool confirmation
with focus while unmounting the composer: a stray Enter aimed at the
prompt approved the background command. Auto-open now lands in list mode
with the row selected; entering detail stays an explicit navigation.

Tests: flip the surfacing contract to list mode, add the masked-approval
regression (red on the single-id latch), drop a dead assignment (R1-8).

* fix(cli): disarm auto-opened task dialog and latch only the displayed approval head

An auto-opened background-task dialog unmounts the composer and keypress
dispatch is a broadcast, so a letter still aimed at the composer could
cancel (x) or pause (p) the selected workflow without confirmation, and a
stray Enter could arm the approval. Tag auto-opens and disarm x/p/Enter in
the dialog's own list branch until the first explicit navigation key
acknowledges the open; user-initiated opens keep immediate behavior.

The surfacing latch recorded the first unsurfaced approval, but the detail
view renders only pendingApprovals[0]; an approval parked behind a still-
parked head was marked surfaced without ever displaying and never prompted
once it became head. Latch only the displayable queue head so the next
approval prompts after the head drains.

Addresses the two Criticals from the #16432 human review.

* test(cli): pin the closed-dialog guard on the approval auto-surface effect

The surfacing effect's dialogMode guard had no test that failed without
it: every existing case fired refreshes only while the dialog was
closed. Add a case that opens the detail view, parks a workflow approval
mid-read, and asserts the view and selection stay put. Deleting the
guard turns exactly this case red (#16432 R2-3).

* fix(cli): hold the auto-open disarm until a real navigation key

- Acknowledge the auto-open only on arrow keys. The selection matchers also
  carry bare k/j and Ctrl+P/Ctrl+N, and ordinary typed letters satisfy
  them, so prose typed at the composer the dialog just unmounted released
  the disarm and the next x/p/Enter reached the run (#16432 R2-1).
- Swallow closeDialog while the auto-open is unacknowledged: Escape, Left
  and Ctrl+C burned the add-only latch before the prompt was ever seen and
  the workflow fell back to the badge-only hang this PR removes. Ctrl+C
  reaches the same path through closeAnyOpenDialog (#16432 R3-1).
- Unfocus the pill in the auto-open writer, matching openDialog, so the
  pill's Enter cannot run openDialog() and clear the disarm (#16432 R3-2).
- Advertise only "arrows activate" while disarmed instead of Enter view /
  x stop, which were swallowed (#16432 R3-3).
- Record the parked head ids at close so an acknowledged close stays
  closed while another workflow is already parked, and a genuinely new
  approval still surfaces (#16432 R3-5).

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw5b11gghf

* style(cli): wrap the over-width hint condition to the prettier limit

The R3 hint-row re-indent left the paused-agent condition one column
over the 80-char limit, failing the Lint & Static gate at ac4c4d7a97.
Formatting only; no behavior change.

Refs #16432

* fix(cli): swallow bare r while the auto-open disarm is armed

An auto-open lands mid-keystroke while the user is still typing into the
composer the dialog just unmounted. Bare k/j still move the selection by
design (R2-1), so prose can park the selection on a paused agent; the
following `r` of the same sentence then reached resumeSelected() and
restarted a run the user deliberately paused. Add `r` to the same
deny-list that already swallows `return`/`x`/`p` until a real arrow key
acknowledges the auto-open (#16432 R4-2).

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw5jlokths

---------

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-07 10:49:33 +08:00
49ac6dae86 fix(sdk): name daemon UI tool blocks from _meta.toolLifecycle frames (#16306)
* fix(sdk): resolve tool name from _meta.toolLifecycle for web-shell labels

Foreground subagent tool calls render as "unknown Tool" in the web-shell
session view because the daemon normalizer's tool-name chain never reads
`_meta.toolLifecycle.toolName`, the only name carrier on lifecycle frames
emitted by transcript-replay. Such a frame projects a block with no
toolName and the generic "Tool" title, and the web-shell adapter's
`resolveToolCallName(...) || 'unknown'` renders that pair verbatim.

Append the lifecycle name to the tail of the resolution chain so explicit
`title` / `toolName` / `_meta.toolName` keep their precedence, and leave
the silent-shell lifecycle discard guard untouched.

Fixes #16301

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-issue-patrol/jmw20fhpuwp

* fix(sdk): name subagent tool blocks from lifecycle metadata

The metadata-only guard dropped the one frame that carries a subagent
tool call's name, so the previous fallback never ran and #16301's
"unknown Tool" label survived. Forward a name-only tool.update from those
frames and let the reducer name an existing block by toolCallId: it never
creates a card and never replaces a human-readable title.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw23adg4cx

* fix(sdk): close R2 review on lifecycle tool naming

- Skip name-only tool.update events in the terminal renderer: they carry no
  status, so rendering one printed "tool undefined <name>" per tool call.
- Return before the copy-on-write path when a nameOnly frame targets an
  already-named block, and keep retainedBytes accounting exact on rename.
- Add the status-bearing lifecycle arm test and the nameOnly renderer test;
  both go red when their guard is removed.
- Rewrite the comment above the lifecycle branch, which still claimed those
  frames are dropped.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw2opz8jds

* fix(sdk): name mid-run attach tool blocks from the lifecycle frame

R3-1: the name-only arm returned early when no block existed for the call
id, so a client that attached mid-run (lifecycle frames before any create
frame) still showed "unknown Tool". Park the late name per call id and
seed the create path with it.

R3-2: read the lifecycle fallback through extractToolLifecycle so a
status-bearing frame makes the same v1 validation decision as the
metadata-only arm.

R3-3/R1-2: pin the create path with producer-shaped frames (no fabricated
status), the no-overwrite guard, and the v2 rejection case.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw2zfs5be7

* test(sdk): pin the parked-name bound and name what the attach case pins

Review R4-2: nothing pinned MAX_PENDING_TOOL_NAMES or its oldest-first
eviction, so deleting the guard left the suite green. Add a case that
parks 257 distinct call ids and asserts the map stops at 256, `call-0`
is gone and `call-256` survives. Verified load-bearing: disabling the
eviction guard fails it with 257 keys.

Review R4-4: the "mid-run attach" case passes unchanged on the base tree,
because its create fixture carries `title` and `_meta.toolName` — the
pre-existing chain already reads those, so the parked name is consumed
and discarded. Rename it to what it actually pins; the sibling case
`seeds the late name when the status frame carries none` is the one that
discriminates the parking path.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw3p5pfkf6

---------

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-07 10:42:48 +08:00
易良andQwen-Coder 8660f75430 fix(ci): refresh host assets on older PR checkouts (#16476)
* fix(ci): refresh host assets on older PR checkouts

Co-authored-by: Qwen-Coder <[email protected]>

* style(ci): format host asset regression cases

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-07 01:42:26 +08:00
易良andQwen-Coder cb282ef5cf test(web-shell): unit-test settings development route (#16456)
Replace the full Vite dev-server startup with a direct middleware test so the settings route contract no longer depends on Babel and App.tsx cold-start time.

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-07 00:44:10 +08:00
0964c4bc3c test(serve): cover the workspace_runtime_stop boot-time conjunction (#16469)
The stop surface is advertised only when five boot-time legs hold:
managedChildProcesses, workspaceRuntimeRemoval, a live ACP http handle,
manageScheduledTaskSessions, and a managed runtime whose bridge exposes
stopWorkspaceRuntime/getRuntimeStopCompletion/getRuntimeStopSnapshot.

Nothing exercised that computation from /capabilities: the only existing
references inject the workspaceRuntimeStopAvailable toggle directly, so
any single leg could regress while the suite stayed green. The new test
mounts the real app with every leg wired and asserts the feature is
advertised, then drops one required leg and asserts it is not.

Part of #12563, does not close it.


Patrol-Run: qwen-issue-patrol/jmw51qoerzl

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-07 00:42:42 +08:00
易良andQwen-Coder e61ad3959e fix(managed-agent): settle a title the dispatcher claimed first (#16447)
* fix(managed-agent): settle a title the dispatcher claimed first

renameSession admits the RENAME row and then claims it. The background
SessionRenameCoordinator scans rows a request has just admitted, so its
claim can win without any concurrent caller, and the request answered
503 "The Session title is already being delivered." for its own rename.

Wait a bounded 2s for that delivery to settle and answer the recorded
command instead of failing the request.

Fixes #16431

Co-authored-by: Qwen-Coder <[email protected]>

* fix(managed-agent): keep the creator gate on the rename settle arm

The settle arm answered the recorded rename outcome without re-running
the replay actor gate, although it shares the COMPLETED read-back's
TOCTOU window: a pre-read that missed the recorder's row lets a
non-creator OPERATOR reach the settle through the duplicate-key winner
path and take a 200 the COMPLETED arm refuses with 409. Mirror that
arm's gates — requireReplayActor on the settled Session, and the
last-visible serialization — so both arms answer identically.

Test: a non-creator OPERATOR racing the recorder's key through the
settle arm meets the same 409 workspace_unavailable as through the
COMPLETED arm, while the recorder still replays its own outcome; the
dispatcher's completion is asserted so the refusal provably comes from
the settle arm. Load-bearing: reverting the gate answers the raced
request 200 and fails the test.

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-06 21:15:12 +08:00
易良andQwen-Coder 989d31ee9e test(managed-agent): retarget the lifecycle IT and retry snapshot claim deadlocks (#16446)
The publication-lane IT still parked the receipt thread on the placement
guard's `INSERT INTO ma_tenant_guard`, but #16354 moved ordinary publication
lanes onto the Session root, so that statement never runs and the test now
fails on its own latch instead of on a product lock cycle. Park the receipt
on the root statement its lane actually executes.

The sibling native-snapshot IT exposed a real gap: two publishers of the same
snapshot claim the manifest row with `INSERT ... ON DUPLICATE KEY`, and InnoDB
can resolve that by rolling one side back as a deadlock victim. Retry the
claim a bounded number of times so the loser reads the winner's row and
answers the replay descriptor instead of surfacing a deadlock.

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-06 21:15:04 +08:00
易良 1ecf444010 test(interactive): poll the foreground-shell frame instead of snapshotting it (#16455)
#15712 recurred on 2026-11-05 (run 42069115052, macOS): the case asserted
`compact(frame('mode-before'))` contains `YOLO mode` straight after a PTY
write, and on a loaded host the approval footer had not repainted yet — the
captured frame still held the startup banner, so the assertion failed on a
correct UI. The draft snapshot just above it has the same shape.

Poll both through the suite's `until`, which already scales its budget 3x
under CI (#16276), so a slow repaint is waited out while a genuinely missing
footer still fails on the same message, with the frame kept in FRAMES.json.

Verified locally: the case passes (16.9s) and the whole file passes
(3 passed, 1 skipped).
2026-11-06 20:28:34 +08:00
易良andQwen-Coder 1a20792cfa ci: route the no-AK integration gate to ECS for every pull request (#16132)
* ci: route the no-AK integration gate to ECS for every pull request

Same trust class as lint_and_static (2026-11-02 extension of the
2026-10-31 decision): the integration step executes PR code, but the
lane carries zero secrets by construction — every model key is emptied
at the step and the job's only token is the read-only checkout grant.
The gate's step-level runner.temp HOME becomes run-unique and is
cleared before use (the R2-1 lesson: RUNNER_TEMP survives across jobs
on this pool). test and integration_cli keep the association-gated
ubuntu_runner. Mutation-verified: runs-on revert, fixed HOME, and a
real key in the PR-code step each red the routing battery.

* fix(ci): isolate no-AK HOME from the first step; value-pinned zero-secrets

Round-2 review findings: (1) Critical — the gate-scoped HOME missed the
lane's actual fork-code start: checkout's local action and the install
(lifecycle scripts plus scripts/prepare.js, un-skipped) ran under the
runner's real shared home with a shared COREPACK_HOME — the exact
cross-job poisoning channel the lint acceptance closed. Mirror lint:
first-step GITHUB_ENV run-unique HOME + COREPACK_HOME inside it +
shared verified XDG cache + always() reclamation. (2) Critical — the
zero-secrets guard was name-based and missed odd-named secrets; enforce
by VALUE (every non-path env key empty, no secrets.* anywhere in the
gate). (3) runs-on literal pin tightened (timeout line also mentions
lint_runner). Docs: EN single-lane phrasing, stale merge-queue claim,
2026-11-02 paragraph rewritten for the first-step shape. Six mutations
red (first-step removal, shared corepack, odd-name secret, no cleanup,
runs-on revert, fixed HOME).

* docs(ci): correct the enforcement-order claim and stale trust-basis note

Round-2 review: the doc claimed the routing battery runs before
dependency install — false (HELPER_TESTS imports yaml, runs after
install, and not at all on github_ci_only PRs' own runs); and the
ci.yml trust-basis comment still described the round-1 gate-scoped
HOME shape. State the actual timing and detection latency, and the
shipped first-step shape.

* fix(ci): scrub ANTHROPIC_AUTH_TOKEN at the no-AK gate; honest cache naming

Round-3 review: the Anthropic SDK's auth resolver back-fills
ANTHROPIC_AUTH_TOKEN from env, so a runner-account residue would survive
into fork PR code on the pool — add it to the gate's scrub list (pinned
by both suites). The 'Configure persistent npm cache' step name promised
persistence the per-run HOME removes; rename it and record the accepted
cold-install cost (shared pnpm store stays a follow-up).

* fix(ci): scrub ANTHROPIC_AUTH_TOKEN at the no-AK gate; honest cache naming

Round-3 review: the Anthropic SDK's auth resolver back-fills
ANTHROPIC_AUTH_TOKEN from env, so a runner-account residue would survive
into fork PR code on the pool — add it to the gate's scrub list. The
value whitelist only iterates present keys, so the deletion mutation
escaped it; pin the key's presence-and-empty in the battery and the
28-key literal list. The 'Configure persistent npm cache' step name
promised persistence the per-run HOME removes; rename and record the
accepted cold-install cost (shared pnpm store stays a follow-up).

* test(ci): pin the no-AK grant, HOME template and reclamation path

Three assertions in the no-AK routing battery proved less than they read:

- the job's `permissions` was never inspected, so the read-only-checkout
  grant that justifies routing untrusted fork code onto the pool could be
  widened without a red test;
- `create.includes('HOME=')` is satisfied by `COREPACK_HOME=` and
  `XDG_CACHE_HOME=`, so deleting the real HOME/USERPROFILE publication kept
  the test green;
- the cleanup only had to contain the `qwen-no-ak-home-` prefix, so a path
  that dropped `-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}` would still pass
  while removing a directory nothing used.

Pin the grant by value, the publication by its printf template, and the
reclamation by the exact run-unique path the create step builds.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw48g5gafx

* test(ci): pin the realized HOME values in the shared ungated lane loop

R2-1: assert the create `home=` line, the gate QWEN_HOME and the cleanup
command by value, and require the exact set of `$GITHUB_ENV` writers. An
`echo` stand-in for the cleanup, a `-v2` create path and an appended second
HOME writer all kept the battery green under containment checks.

R1-2 / R1-1: hoist those pins into the shared `ungated` loop so
`lint_and_static` gets them too. Its create step was pinned by
`includes('HOME=')` (satisfied by `COREPACK_HOME=`) and its cleanup by
`includes('qwen-')`, so a dropped `HOME=%s` or a retargeted `rm -rf` survived.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw4h0sl6g9

* test(ci): red on later republishes of the isolated HOME keys

R3-1: the GITHUB_ENV equality guard read step 0 only, so a later step that
republished HOME reverted the run-unique home by last-write-wins while every
assertion stayed green — the isolation would silently stop applying to the
steps that run PR code. Scan the remaining steps for isolated-key
$GITHUB_ENV writers and for step-level HOME/USERPROFILE/COREPACK_HOME/
XDG_CACHE_HOME overrides, leaving the npm cache step's own key a legitimate
later write.

Mutation-verified on this head: appending printf 'HOME=%s\n' ... to
integration_no_ak's 'Configure per-run npm cache (self-hosted)' step and
adding a step-level `env: HOME` each turn the suite red (103 pass -> 102
pass / 1 fail); reverting restores 103/103.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw4ng9wvgi

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-06 16:41:48 +08:00
2666e3552d fix(core): make persisted session pagination safe for equal mtimes (#16024)
* fix(core): make persisted session pagination safe for equal mtimes

listSessions() ordered files by mtime alone and resumed the next page with a
strict `mtime < cursor`. An mtime is not unique — bulk copies, backup restores
and coarse-timestamp filesystems all produce ties — so a group of files sharing
an mtime that straddled a page boundary was dropped while hasMore still reported
progress, then the next page came back empty: 25 files at one mtime with size 5
showed 5 items in total.

Sort on (mtime desc, name asc) so the order is total, and let the cursor carry
that position: nextCursor is now `"<mtime>|<name>"`. A bare number is still
accepted with its existing strict `<` meaning, so callers holding a legacy
cursor keep working, and the daemon's opaque string cursor now passes the
composite form through instead of rejecting it as invalid.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-issue-patrol/jmvwpo7jxrk

* fix(cli): carry the composite session cursor type through callers

listSessions() now hands back a "<mtime>|<name>" cursor so a page boundary
inside a group of equal mtimes does not drop sessions. The widened
`number | string` contract left the CLI callers that store or compare that
cursor typed as `number`, which is a type error in `tsc --build` even though
nothing changes at runtime.

Widen the three consumers to `number | string`: the daemon's persisted scan
cursor, the live coordinator's paging plus its loop guard, and the session
picker state used by both the ink and opentui pickers. No behaviour change.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-issue-patrol/jmvwrtdc1rm

* fix(cli): route composite session cursors to the persisted listing

The persisted listing mints `"<mtime>|<name>"` cursors when a page ends
inside a group of files sharing an mtime, but the route still chose between
the persisted and the live listing path with a numeric-only predicate. A
composite cursor therefore fell through to the live path, whose parser only
accepts numbers, and the second page answered 400.

Export one predicate from session-list.ts carrying the same shape test the
cursor parser already applies, and use it for that routing decision.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-issue-patrol/jmvwtyj3bro

* fix(session-list): keep composite cursors usable by ACP and VS Code clients

listSessions mints "<mtime>|<name>" cursors whenever a page ends inside a
group of files sharing an mtime, but two consumers still parsed every cursor
as a number: ACP session/list rejected page 2 with "not a valid numeric
cursor", and the VS Code companion turned the composite cursor into NaN so
hasMore read false and only page 1 was ever listed. A malformed "|name"
cursor also slipped through the persisted-cursor classifier and answered 200
with an empty page instead of invalid_cursor.

- acpAgent: accept and forward "<mtime>|<name>" cursors to listSessions
- session-list: require a non-empty mtime prefix before classifying a
  cursor as persisted
- vscode companion: pass the daemon cursor back verbatim, widen the cursor
  type and treat only a non-empty cursor as "append to the current page"

Tests: ACP composite passthrough + empty-prefix rejection, VS Code
getSessionListPaged composite nextCursor round trip.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvxke6bm6i

* fix(vscode): keep composite session cursors type-safe in the companion

021698420c widened the companion's paged session cursor to number|string but
left AcpConnection.listSessions typed number-only, so the package build failed:
TS2345 at qwenAgentManager.ts:573 and TS2365 at :626.

- accept the daemon's "<mtime>|<name>" cursor in AcpConnection.listSessions,
  which already forwards String(cursor) to ACP
- derive the mtime floor from the cursor prefix in the file-system fallback;
  the previous comparison coerced the composite string to NaN and silently
  returned an empty page

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvxmjc316l

* fix(session): align persisted-cursor docs, error kind, and tie-break test

- test(core): feed readdir a non-name order so deleting the sort tie-break
  fails the equal-mtime pagination test (mutation-verified: 9/25 without it)
- fix(cli): report a persisted cursor as such instead of "numeric" in
  InvalidCursorError, matching the message ACP already uses
- docs(cli): describe the opaque persisted cursor in the serve protocol doc
- docs(core): drop the mtime-only-cursor rationale this change removed

Declined: folding the cursor codec into one validated helper across core and
the transports — a cross-package API change outside this PR's scope.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvzvzfp79t

* docs(session): describe composite persisted cursors on every continuation page

`listSessions` mints `"<mtime>|<name>"` whenever a page has more results, so
the protocol text must not suggest a bare numeric response. The standalone
list route dispatches through the metadata parser, so its invalid-cursor test
should pin the metadata-kind message instead of the persisted one.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw009r8j9z

* test(session): pin the composite persisted cursor shape and narrow its docs

R3-1: the cursor-contract sentence claimed the numeric-cursor input and the
"<mtime>|<name>" reply for every persisted-backed list, but view=organized and
the parentSessionId / sourceType filtered lists page through their own opaque
activity cursors and reject a numeric cursor with 400 invalid_cursor. Narrow
the subject to the legacy recent list and name the other cursor families.

R3-2: no assertion pinned the documented shape. Every persisted-path cursor
assertion was shape-agnostic, so a regression back to a bare numeric mtime
would have kept the suite green. Assert the shape on a page whose boundary
mtime is unique; mutating the mint site to String(lastProcessedMtime) turns
this assertion red.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw04k2rma5

* docs(daemon): name the persisted list cursor in the SDK catalog guide

The batch-cursor sentence kept the legacy "numeric list cursors" wording while
the list route now mints the opaque `<mtime>|<name>` token, so an SDK
integrator parsing the returned cursor got `NaN` and a cursor they could not
send back. Match the protocol doc and point at the persisted list cursors.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw1fplwqc0

---------

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-06 15:43:21 +08:00
45f71b3714 fix(ci): apply the apt-mirror probe to the release docker lane too (#16278)
* fix(ci): apply the apt-mirror probe to the release docker lane too

The release docker integration lane builds the same sandbox image as the E2E
leg but never selected an apt mirror, and it prunes the builder cache on every
run so its apt layers are always cold. On mainland pool hosts that rebuilt
against the throttled deb.debian.org path (11-45 kB/s) — the same defect #16214
fixed for E2E, left unfixed here because it was out of scope (#16271).

Extract the probe (apt_mirror_probe_rate / select_apt_mirror) and a new
apply_apt_mirror helper into a shared .github/scripts/apt-mirror-probe.sh,
sourced by both run-e2e-tests.sh and run-release-docker-integration.sh. Each
calls apply_apt_mirror before build:sandbox, so a mainland host builds against
the VPC intranet mirror (6-9 MB/s, verified for E2E) while a healthy default is
left untouched. The Dockerfile APT_MIRROR ARG already exists from #16218.

Contract tests read the shared probe and pin the candidate loop and the source
statement (not substrings the comments also satisfy); both callers must apply
the probe before the build. Mutation-verified: removing the release call, a
candidate, or a source line each reddens the suite.

* fix(ci): carry the apt-mirror probe into the shard-retry test rig

run-e2e-tests.sh now sources its sibling apt-mirror-probe.sh at startup.
The shard-retry meta-test copies only run-e2e-tests.sh into a temp dir, so
the source resolved to a missing file and 'set -e' aborted before any shard
ran — all eight cases saw zero npm calls. Copy the probe file alongside the
script in the rig so the source resolves; sourcing only defines functions
(no network at source time) and the sandbox:none path never calls
apply_apt_mirror, so the rig stays hermetic.

Witness: CI at 315733fe showed all 8 e2e-shard-retry cases failing
(npmCalls expected 2, got 0); with the probe carried into the rig all 8
pass locally. eslint exit 0, prettier clean.

* fix(ci): address #16278 review — hermetic probe test, shebang, scope header

R1-1: the release-lane exec harness now stubs curl (per-URL rates) so the suite
no longer probes the live network; the npm stub logs BUILD_SANDBOX_FLAGS and two
execution tests assert the fast mirror is passed as --build-arg APT_MIRROR with
the CI label preserved, and that an all-slow probe adds no mirror arg. Flipping
the mirror-selection guard reddens them.

R1-2: add the inert shebang so file(1) reports text/x-shellscript and the
shellcheck lane enrols the sourced probe; a lint test pins the mime type.

R1-3: narrow the header to the two covered build sites and record the third
cold caller (release-sdk.yml build-sdk) as out of scope, tracked in #16317.

* test(ci): drive the shellcheck lane for the probe; copy scripts dir in shard harness

R2-1: the shebang test now runs the lane's own candidate selection with only
shellcheck stubbed, so a filter change dropping the probe is caught (removing the
shebang reddens it). R2-2: the shard-retry harness copies the whole .github/scripts
dir so any sourced sibling comes along instead of a hand-maintained file list.

* merge(ci): sync origin/main to pick up the pinned COREPACK_HOME gate

The lint-gate-freshness lane went red on 9b3d131cdb because main moved
.github/workflows/ci.yml at 2b9c4a98f4, and the shard-4 Linux lane died on
the very corepack 'pnpm.mjs' miss that commit fixes.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw2t0at7dy

* docs(ci): stop promising the release lane a benefit its routed host cannot get

`integration_docker` is pinned to `ecs-qwen-hk4-host`, or `ubuntu-latest` when
the ECS pool is unavailable. On the Hong Kong host `deb.debian.org` already
answers at MB/s and the VPC mirror is unreachable, so `select_apt_mirror`
correctly keeps the built-in source; on a hosted runner the same is true. The
call-site comment asserted a mainland benefit this call site cannot obtain, and
the always-cold builder cache it sits next to is unchanged.

The comment now says what the call does: apply the same probe the E2E leg uses,
so a host whose default is throttled builds against a faster endpoint, while a
host whose default is already fast keeps it. The `Fixes #16271` claim is
narrowed to `Refs #16271` in the PR body — the always-cold half stays open.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw420o4bfo

* fix(ci): keep the release docker lane script executable

The previous delivery of 6218478b62 went through the Git Data API, which
recorded .github/scripts/run-release-docker-integration.sh as 100644 while
the base has it 100755. release.yml runs these by bare path, so the mode is
load-bearing, and scripts/tests/release-workflow.test.js
> keeps the extracted release scripts executable already fails on it
(Linux unit tests shard 3/8 on 6218478b62).

Restore the recorded mode to 100755.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw48g5gafx

* test(ci): stage the probe fixture at its real path (#16278 R2-1)

The rewritten case kept its fixture at the temp-repo root, where the
lane's `^([^.]+)` candidate alternative already matches, so deleting the
`.*\.(sh|zsh|bash)` arm left it green. Put the file at
`.github/scripts/apt-mirror-probe.sh` — a leading dot makes the `.sh` arm
the only way it can be selected — teach `setup()` to create parent
directories for nested names, and assert the exact shellcheck argv.

Two-arm check on the lane's own run string: as authored, exit 0 with
`... --color=never .github/scripts/apt-mirror-probe.sh`; with the `.sh`
arm deleted, exit 1 ("matched no shell-script candidates").

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw4cqh0dg3

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-11-06 15:42:59 +08:00
易良andQwen-Coder 7c6f93ca51 fix(cli): forward the folder-trust store into the container sandbox (#16390)
* fix(cli): forward the trusted-folders store into the sandbox

The ACP hook tests red only in the docker lane since the folder-trust
feature was enabled in their child home (#16264): the rig grants the
workspace through QWEN_CODE_TRUSTED_FOLDERS_PATH, but start_sandbox never
passed that path through. The container fell back to
<QWEN_HOME>/trustedFolders.json, found no trust decision, and the settings
layer dropped the workspace scope, so the approved project hooks never
reached the registry.

Forward the override like the MCP and hook approvals paths, translating
the Windows path first.

Co-authored-by: Qwen-Coder <[email protected]>

* test(serve): allow the trust-store read in the sandbox env guard

The sandbox launcher now reads QWEN_CODE_TRUSTED_FOLDERS_PATH twice while
forwarding it, and the serve process.env guard pins every access. Register
the new read in the launcher's allowance so the guard suite stays green.

Co-authored-by: Qwen-Coder <[email protected]>

* test(serve): pin unset sandbox env stores

Cover every forwarded approval/trust store in the unset branch, so dropping a guard cannot silently pass "undefined" into the container.

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-05 22:46:14 +08:00
c8eaa04e81 test(review): pin the carried-ledger carrier order and fail-open arms (#16395)
Four test-only gaps in dedup-candidates.test.ts; no production change:

- a claim both carriers hold is dropped once and cites the POSTED
  carrier, not the artifact's mirror of it (carried is posted-then-
  deferred), with both halves asserted loaded so the order is what
  decides the citation
- the side-file read fails open on its own two arms, which the absent-
  file case does not reach: a truncated JSON file lands in the parse
  catch, and a non-array `findings` / `round: 0` file is refused by the
  shape guard — no throw, no drop either way, each with a control that
  the same claim DOES drop when the file is intact
- a kept candidate reaches kept[0] with every extra field verbatim, so
  the Step 4 verify shards can still be built from the kept list
- the deferral half's three keep arms (far anchor, loose overlap,
  Critical candidate vs non-Critical entry) each keep, with a control
  that the same pair still absorbs under matching input

Every new case was verified to have teeth: each was pushed red against a
mutated production behaviour (carrier order swapped, parse catch
rethrowing, each shape-guard leg removed alone, kept narrowed, anchor
tolerance widened, similarity bar removed, severity guard removed) and
the production file was restored byte-identical afterwards.


Patrol-Run: qwen-issue-patrol/jmw3k5nxoy7

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-05 22:41:53 +08:00
易良andQwen-Coder b87d60ddf0 fix(serve): serve qualified hooks status for untrusted workspaces (#16388)
* fix(serve): serve qualified hooks status for untrusted workspaces

The workspace-qualified hooks status GET sat behind the trusted-runtime
gate alongside env/preflight, so with folder trust enabled an untrusted
workspace got 403 exactly when the Web Shell approval card needed the
pending decision. The card treats the failed read as no hooks and never
renders, leaving no interactive way to approve. Route the hooks read
through the same trust-free resolver as the qualified approve POST;
env/preflight keep the trusted gate.

Fixes #16387

Co-authored-by: Qwen-Coder <[email protected]>

* test(serve): pin pending hooks decision for untrusted workspace

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-05 22:00:59 +08:00
易良andQwen-Coder 4e75789d8e test(web-shell): pin the rewind capacity-latch re-open (#16358)
* fix(web-shell): defer rewind capacity-latch re-open publish past the rebuild commit (#16347)

A rewind drops the newest blocks, and the rewind rebuild that follows
replays the surviving prefix synchronously. Publishing the capacity-latch
re-open synchronously inside the truncation callback let a concurrently
batched same-anchor page append land before the rebuild's prefix, which
macOS CI observed once as a block-order inversion in
DaemonSessionProvider's 'reopens a rejected history page after
authoritative rewind with same anchor'.

Keep the latch ref updates synchronous (loadMore gating and synchronous
readers still see the reopened latch immediately) and defer only the
React state publish one microtask, past the rebuild commit. Oldest-first
trims keep the fully synchronous path.

Co-authored-by: Qwen-Coder <[email protected]>

* test(web-shell): pin the rewind capacity-latch re-open and correct the fix comment

Addresses two review findings on the #16347 fix:

- The deferral was unpinned: no test could distinguish the deferred publish
  from the previous synchronous one. Add a case that latches the capacity
  rejection, commits a rewind rebuild, and asserts the latch re-opens once
  the rebuild frees the window. A regression that drops the truncation
  reconciliation on the rebuild's store.reset() turns this red while the
  pre-rewind latch assertions stay green. The deferred-vs-synchronous
  distinction itself is not observable through React act() microtask
  flushing, so the test documents that it pins the re-open contract, not
  the queueMicrotask mechanism.

- The comment described the rebuild as following the publish; the rewind
  reducer installs the surviving replay prefix BEFORE invoking the
  truncation callback. Restate the ordering against the store commit and
  the notify microtask.

Co-authored-by: Qwen-Coder <[email protected]>

* test(web-shell): pin the rewind capacity-latch re-open

Drop the one-microtask deferral of the rewind latch publish: the
truncation callback runs from the SDK reducer after state.blocks is
sliced, and the publish carries only latch metadata, so a microtask
cannot reorder any store-level step. The added test stays - it pins
that a rewind rebuild re-opens the capacity latch and the rebuilt
window keeps only the surviving prefix.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw3caqrueo

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-05 19:51:00 +08:00
c0782a8cb6 test(ci): pin the zero-skip timeout shape in awaitTwoSkipLines (#16384)
Refs #10737


Patrol-Run: qwen-issue-patrol/jmw3fvcf8y3

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-05 11:45:43 +00:00
易良andQwen-Coder 6fe5bca8e1 test(managed-agent): match the busy-storage message in ChildWorkspaceServiceTest (#16382)
`WorkspaceExecutionStore.busy()` changed its message to
"Workspace is not available for this operation." in #16369, which updated the
in-test stub at line 1345 but not the `aBusyStorageParksTheStepWithoutSpendingAnAttempt`
assertion at line 707. The assertion still expected the old "held by another tool
turn" text, so the test now fails deterministically on the new message (seen on
run 41981102231, both attempts). Update the assertion to the new busy message.

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-05 19:30:31 +08:00
易良andQwen-Coder feb06ca823 test(managed-agent): declare v2 reader in cleanup-conflict MySql IT (#16372)
`HostedCleanupRetentionMySqlIT.cleanupAdmissionConflictIsReturnedOverHttpOnMySql`
expects 409 `managed_session_cleanup_conflict`, but its commit carries a
`hosted.cleanup` event, which `ManagedHostedRecoveryRecords.requiresVersionTwo`
classifies as requiring storage v2. `JournalCommit` then calls
`readerVersion.requireReadable(2)` before the cleanup-conflict check, and the
request never set the `X-Qwen-Managed-Max-Readable-Storage-Version` header, so
`ManagedSessionReaderVersion` defaulted the ceiling to 1 and threw
`managed_session_reader_version_unsupported` first. The 2026-11-04 nightly
failed on exactly that mismatch.

Declare the v2 reader ceiling (the same header `ManagedSessionStoreIntegrationTest`
uses when it expects the commit to be accepted) so the request reaches the
cleanup-admission path the test actually exercises.

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-05 16:27:17 +08:00
406258d66a fix(core): headless project-hook trust requires the folder-trust feature (#16264)
* fix(core): headless hook trust requires the folder-trust feature

The reporter PoC runs headless (--output-format stream-json with a
one-shot prompt), so the unconditional headless exemption left the
reported execution path open on a fresh default environment. Require
the operator to have enabled security.folderTrust.enabled before the
workspace choice counts as implicit trust; with the feature off,
project hooks are withheld in every mode. Embedders enable the feature
in their managed home settings and keep working via their trust store.

Verified locally against the reporter scenario: default fresh home ->
marker absent; feature on + trust-store grant -> marker written.

Refs #14746

Co-authored-by: Qwen-Coder <[email protected]>

* refactor(core): unify the project-hook gate on explicit trust

The headless exemption branch never changed an observable outcome: the
CLI settings layer already drops workspace settings for an undecided
folder once the feature is enabled, and with the feature off the
explicit predicate is false anyway. Replace both branches with the
single explicit-trust rule so interactive and non-interactive callers
share one contract. Behavior is unchanged; verified against the
reporter PoC (default fresh home withheld, trusted path executes).

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): address review findings on the unified hook trust gate

R1-1: the headless feature-off test wrote folderTrust into workspace
settings, where WORKSPACE_RESTRICTED_SETTINGS strips it — the rig user
scope stayed enabled and the test never reached the named gate. Write
the feature-off decision into the user-scope home file instead.

R1-2: the docs sentence overclaimed; IDE-granted trust loads hooks with
the feature off. State the predicate as shipped.

R1-3: headless withholding was silent for the newly gated population.
Emit one stderr line from the non-interactive entry and a debug-log
warn from the registry when the trust gate is why the scope is absent
(getTrustWithheldProjectHooks, diagnostics only).

Verified: reporter PoC withheld with the stderr line present; core
1052 passed; tool-hook integration 14 passed.

Co-authored-by: Qwen-Coder <[email protected]>

* feat(web-shell): surface trust-withheld hooks in the workspace overview

The hooks listing already marks why a project hook is disabled, but the
ACP status serializer dropped the reason, so the web-shell overview only
showed a count while trust-withheld hooks silently did not run. Carry
disabledReason through ServeHookEntry/DaemonHookEntry (optional, older
daemons unaffected), count untrusted hooks in the summary, flag the
facet, and name the remedy in the tooltip (en + zh).

Co-authored-by: Qwen-Coder <[email protected]>

* test(web-shell): pin the trust-withheld hooks tooltip wording

Component-level witness for the untrusted-hooks facet: the row shows
the withheld count and the title names the remedy. The sidebar hooks
facet is opt-in for embedding hosts, so the jsdom suite is the
reachability-independent pin.

Co-authored-by: Qwen-Coder <[email protected]>

* test(cli): align fixtures with the surfaced hook state

The workspace-hooks status test now expects the disabledReason the
serializer carries, and the headless-warnings config stub provides the
new getTrustWithheldProjectHooks accessor.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): tolerate partial config stubs in the headless hook notice

The resumed-transcript and stream-json housekeeping tests use minimal
config stubs; optional-chain the two accessors the notice reads, and
provide the accessor on the remaining isInteractive stubs. All 124
llm.test.tsx cases pass.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(hooks): make the withheld-hooks surfaces reachable and equivalent

Review findings on the status surfaces: the listing never carried
disabledReason untrusted for settings project hooks (the trust gate
keeps them out of the registry, and only agent/session rows were
trustGated), so the web-shell hint was unreachable; buildHooksListing
now appends the diagnostics block as disabled (untrusted) rows, with a
real-listing test. The headless stderr notice now mirrors the TUI
predicate (raw workspace hooks non-empty, exclude disableAllHooks and
safe/bare), covering the feature-on undecided case the Config accessor
cannot see. The facet value stays language-neutral; the localized
withheld count and remedy live in the tooltip. Core 1035, llm 124,
web-shell sidebar 51 tests pass.

Co-authored-by: Qwen-Coder <[email protected]>

* test(web-shell): browser witness for the withheld-hooks status surface

Real Chromium e2e: open Daemon Status via the rail More popover, switch
to Diagnostics, and assert the workspace.hooks section renders with the
withheld count from a report shaped like the real daemon output.
Screenshot retained as test output.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(hooks): fold the deferred approval follow-ups into this PR

Per review direction, land the #16295 items here instead of a follow-up:

- Explicit projectHooksApproved state: a stored approval keeps execution
  authority inside a command-line --yolo session (previously the trust
  gate withheld it because the pending flag is undefined while exempt).
  The flag never doubles as nothing-to-approve; mid-session decisions
  update it. Tests pin yolo+approved executes, yolo+unapproved withholds.
- Display-only raw workspace block: with the feature on and the folder
  untrusted the settings layer drops the workspace scope, so the listing
  could not show the withheld hooks. projectHooksDisplayOnly feeds only
  getTrustWithheldProjectHooks, never the registry; test pins both halves.
- Variable-source wording: the dialog hint now says environment (project
  .env/settings.env or shell), not only the approved project env.

Per-hook approval granularity stays out: it is a feature iteration, not
a correctness fix. Core 1017+1053, cli config 565, approvals 50, llm 127
pass.

Co-authored-by: Qwen-Coder <[email protected]>

* style: prettier the reworded dialog hint

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): /hooks list shows withheld project hooks in ACP/web-shell

The non-interactive /hooks list read only the registry, so an ACP
session (web-shell) with unapproved project hooks answered \"No hooks
configured\" — the user could not see what was withheld or why. The
list now includes the approval/trust diagnostics blocks, marked
disabled (not approved / rejected / folder not trusted). Verified
against a real ACP session: the withheld SessionStart hook is listed
with its reason. Tests pin all three reasons.

Co-authored-by: Qwen-Coder <[email protected]>

* feat(web-shell): explicit project-hooks approval prompt for withheld hooks

Web Shell previously had no interactive surface for the project-hooks
approval gate: users had to run /hooks manually. The daemon now reports
the pending decision (with sanitized hook lines) on /workspace/hooks,
including on a cold daemon and when folder trust withholds the workspace
scope, and a dedicated POST /workspace/hooks/approve route (plus the
workspace-qualified twin) records the same hash-bound decision the TUI
dialog writes. The Web Shell renders an Approve / Keep-disabled banner
from that state. This is deliberately not session/request_permission:
automation clients auto-answer permission prompts, a trust decision must
be an explicit user act, and approving never grants folder trust.

Co-authored-by: Qwen-Coder <[email protected]>

* refactor(web-shell): render hooks approval with the shared ToolApproval card

Replace the custom full-width alert banner with the existing ToolApproval
card in the standard approval slot above the composer, so the hooks trust
decision looks and behaves like every other approval: same panel, option
list, keyboard handling, and safe-default focus. The card supplies a
project_hooks request with decision-specific option labels; ToolApproval
localizes its heading and question for that tool name.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(web-shell): hooks approval probe reads once and polls only while pending

The approval card polled /workspace/hooks unconditionally, adding
background metadata reads the workspace-overview quiet-period contract
forbids. Read once per mount/workspace/settings change, poll only while a
decision is pending, and let the E2E quiet-period test settle the initial
probe before counting sidebar traffic.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(hooks): apply the persisted decision once and to every live session

Local review + ponytail over the PR diff:

- The ACP child no longer re-derives and re-writes the approval hash on
  the approve control. The daemon persists the decision for the exact
  block the user reviewed; a second write could bind it to a block that
  changed since the review. The child only applies the decision, to
  every live session config, not just the bootstrap one.
- The raw-workspace review subject hashes the project env files
  regardless of current folder trust, so a decision recorded while
  untrusted still matches after the folder is trusted.
- The headless trust warning no longer doubles with the approval-gate
  warning for a pending block; the registry debug-log duplicate and its
  interface member are removed (the stderr line and listings already
  report the withheld state).
- Drop dead test stubs for an accessor llm.tsx never reads, a redundant
  type member, and a stray blank-line deletion.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(hooks): scope the approve decision to the admission-bound workspace

The per-session fan-out matched every live config in the child. Key it
on the admission-bound workspace root (the same root the settings
handlers use) against the request's cwd instead: session/cd moves the
live target dir, and a decision recorded for one workspace must not
clear or set the gate on a session admitted under another. Regression
test covers same-workspace, cd-moved, and foreign-workspace configs.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): bind the project-hooks approval to the reviewed block

The daemon's approve route re-read .qwen/settings.json at click time and
persisted the hash of that fresh read, while the ACP child applied the
decision to the block it rendered at startup. A file change inside that
window (git pull, another session, the workspace's own agent) recorded an
approval for hooks the user never saw, and left the card pending because
the stored hash matched neither side.

The child now persists against the same read that produced the status cell
it rendered, so the reviewed block and the recorded block are the same
object. The daemon only falls back to its own read when no child answered,
which is the cold-daemon case where that read is what the client saw.

Also unbreaks the web-shell lane: App.test.tsx's daemon-react-sdk mock had
no useOptionalWorkspace export, so every App render threw and 1320 tests
failed; and the workspace-overview facet counts no longer include the
per-mount /hooks probe, which broke the exact facet-set assertion.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw2kfno0dl

* fix(cli): explicit folder trust alone runs project hooks

The startup gate demanded a stored approval even when the folder was
explicitly trusted, contradicting the documented either-or authority:
core's readProjectHooks accepts approval OR explicit trust, but the
pending flag short-circuited the accessor before trust was consulted.
A platform that pre-trusts its managed directories (folderTrust enabled
plus a TRUST_FOLDER/TRUST_PARENT entry) saw every project hook withheld
headless. Clear the pending gate for a trusted folder unless the exact
block was explicitly rejected — a recorded 'no' still wins over trust.

Verified headless on a real fixture: fresh home withholds; trust config
alone fires the hook; a rejected block stays withheld in the same
trusted folder. Gate unit tests cover pending/rejected/approved under
explicit trust.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(hooks): bind the approval decision to the rendered block hash end to end

The status cell now publishes pendingApprovalHash, the Web Shell card
echoes it with the decision, and both the daemon route and the ACP child
verify it: the daemon rejects a decision whose hash names a block other
than the current file (409 project_hooks_changed, nothing persisted),
and the child applies a hash-bound decision only to configs holding
that exact startup subject (projectHooksApprovalSubject), leaving any
config with a different block pending. Closes the cold-daemon re-read
window: an approval can no longer bind commands the user never saw.

Verified on a real daemon: render hash -> edit the hook file -> POST
with the rendered hash returns 409 and persists nothing; re-render ->
approve with the fresh hash persists and enables the hook; a real
browser session then fires it. Card tests 5/5 (including hash echo),
approve-control tests 2/2, gate tests 20/20, build + bundle clean.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(hooks): close the remaining approval-gate review findings

- Escape on the Web Shell trust card defers without recording a decision
  (TUI semantics): ToolApproval gains escapeDismisses/onDismiss, and the
  card hides until the block hash changes instead of persisting a
  rejection for a habitual keypress.
- The card's options are *_once kinds so the shared safe-default logic
  pre-selects the reject side; a stray Enter on the security gate can
  never grant hook execution. Decision-specific labels stay via the
  option-id i18n keys.
- The hooks-trust card reports into approvalOverlayActive, so the
  composer is dormant while the card owns the keyboard, exactly like a
  tool approval.
- Under command-line YOLO, replacing the hooks block drops the old
  approval authority: an edited block cannot execute on the previous
  block's approval while the gate is exempt.
- The trust-withheld listing surfaces the display-only block even when
  the empty execution block reads the gate as cleared (feature on,
  folder untrusted) — the state the copy exists for.

Tests: core config + hooks-listing 1055/1055, hooks reload 38/38,
ToolApproval + card 99/99 including Escape-defer and reject-default
regressions; build + bundle clean.

Co-authored-by: Qwen-Coder <[email protected]>

* chore: record merge provenance

The main merge (952e0633) refreshed the lint gate; this marker commit
carries the required co-author trailer for the batch.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(hooks): propagate trust authority to every gate re-application path

Review round at 2a00fb4d: the startup gate honored explicit trust, but
the auxiliary paths re-read only the approval store and re-pended hooks
that trust already authorized.

- applyProjectHooksApproval (the /hooks reload and YOLO-exit path) now
  applies the same either-or rule as startup: explicit trust clears a
  pending block; only an explicit rejection of the exact block wins.
- The startup gate mirrors Config.isExplicitlyTrustedFolder(): an
  IDE-reported trusted workspace counts even with the feature off.
- The headless trust warning stays quiet when the execution accessor
  actually serves the block (approved or trust-authorized), matching the
  TUI predicate; the test mock now carries workspace.settings.hooks.
- The live status cell and the cold-daemon fallback stop prompting when
  the hooks are not actually withheld (explicit trust / served block),
  so a trusted workspace never sees a card whose reject would stop
  running hooks.
- The card's Escape defer is keyed by workspace identity read at dismiss
  time (no reset effect can race it), clears on workspace switch, and a
  failed decision POST renders an explicit retry notice.
- The withheld /hooks listing filters non-event keys and null entries
  like the registry path; the YOLO replacement comment now states the
  fail-closed behavior instead of promising a re-apply that never runs.

Tests: daemon facade covers cold-daemon pending+hash, trusted no-prompt,
hash-match persist, 409 changed-block persists nothing, not-pending;
gate re-apply under trust; IDE trust with feature off; trusted reload
keeps hooks / untrusted edit withheld. cli 1879/1879, core 1055/1055,
web-shell card+ToolApproval 99/99. Real browser: trusted dir runs hooks
with no card and no approval record; untrusted dir card -> Escape defer
(nothing persisted) -> approve -> hook fires.

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-11-05 14:42:13 +08:00
2800ea18da fix(cli): keep project hook approval env inside the workspace (#16316)
* fix(cli): keep project hook approval env inside the workspace

Approved project hooks were silently withheld in the docker sandbox lane.
The workspace trust walk admits an ancestor `.env`, the host hashed it into
`hookApprovals.json`, and the container — which mounts only the workspace —
recomputed the hash without it, so `getState()` returned `pending`.

Narrow `readProjectEnvFiles` to files under the workspace root. `findEnvFiles`
still discovers ancestors for the runtime environment, so only the approval
hash input changes. Hooks in a workspace with no ancestor `.env` keep their
current record, but every hash that covered an ancestor changes once, so those
approvals reset to pending a single time.

Fixes #16313

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-issue-patrol/jmw24pta9wt

* style(cli): satisfy prettier in environment hook-approval test

The incremental Prettier lane selected packages/cli/src/config/environment.test.ts
and rejected the single-line hashProjectHooks call added in fe19387a0f. Formatting
only; no behaviour change.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-issue-patrol/jmw26uz28wv

* fix(cli): resolve the .env link target and reuse isWithinRoot

The approval-hash filter compared a lexically resolved candidate against a
realpath'd workspace root, so a `.env` symlinked outside the workspace still
entered the hash on the host while the link dangles inside the sandbox — the
same host/container divergence this filter removes. Resolve the candidate too,
and use the package's existing `isWithinRoot` instead of a second containment
test that can drift.

The reload fixture now writes its `.env` inside the repo it hashes, so the
fixture reaches the hash again instead of being dead code.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw2bv0jkd9

---------

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-05 02:10:58 +08:00
易良andQwen-Coder 2b9c4a98f4 fix(ci): pin COREPACK_HOME for Linux test shards on cold-HOME runs (#16328)
The Linux unit-test shards set HOME to a fresh per-run directory for
isolation. scripts/tests/corepack-warmup.js (vitest globalSetup) spawns
`corepack pnpm --version` before any worker forks, and on a cold HOME
corepack downloads the pinned pnpm from registry.npmjs.org. The ECS
egress intermittently times out on that fetch (ETIMEDOUT to 104.16.9.34),
which kills the shard after every test file has already passed — today
that produced 7 PR-facing failures across random shard numbers (2/8,
5/8, 3/8, etc.), all on the same self-hosted host.

Install dependencies runs before the test step and warms the corepack
cache under the runner's real HOME, but the test step's HOME override
makes that cache invisible. Pin COREPACK_HOME to a per-run path in both
steps so they share the same cache without touching the HOME isolation.

Per-run, not persistent: corepack does not re-verify extracted entries
on cache hit, so a shared cache would be a poisoning channel (the same
reason lint_and_static keeps its COREPACK_HOME per-run).

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-05 01:19:56 +08:00
ed6760aa2c ci: reclaim qwen-autofix workflow headroom (#16069)
* ci: reclaim qwen-autofix workflow headroom

Co-authored-by: Qwen-Coder <[email protected]>

* ci: keep the qwen-autofix anchors the shape pins slice on

The headroom pass collapsed three comment blocks and merged two
`Full rationale` pointer lines. The workflow-shape tests use those exact
comments as slice boundaries for the real bash they execute, and the
pointer parser reads only the first `qwen-autofix.md#af-NNN` per line, so
four pins went red on Test (ubuntu).

Restore the `# Convergence-break mirror` and `# Convergence-signal circuit
breaker (#10107): the park action on` wording, and let every pointer id
spell its own anchor. No behavior change; refresh .size-baseline and the
doc byte count to the new file size.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvyc99g07m

* docs(ci): restore the rationale the autofix compression dropped

R1-1: af-085 answered only about the allowlist, so the worktree-config
file, its pairing with extensions.worktreeConfig, and the hooks-path
resolution ordering read as dead code next to a KEPT allowlist entry.
Restore both paragraphs in the anchor the yml already points at.

R1-6: the age-sweep comment pointed at af-149, which documents heartbeat
and pid liveness only, while the reclaim reason lived nowhere. State it
at both sweep sites.

R1-3: the design record hardcoded the workflow's byte count, which the
ratchet can invalidate without reddening anything. Name .size-baseline as
the single source of the number instead.

The bytes R1-6 adds land in .size-baseline (451750), so the recorded size
still equals `wc -c .github/workflows/qwen-autofix.yml`.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw0wf5wvb8

* fix(ci): repoint two hollow rationale pointers and restore two rationales

Address the four blocking hollow-pointer findings on the autofix size
extraction (each verified by extracting the anchor span at this head):
- gh-config mint (~4980): af-112 documents the git preamble, not the gh
  pins; repoint to af-149, which carries the GH_HOST/hermetic_gh material.
- heartbeat rm -rf (~3742): af-149 lacks the '-rf, not -f' doctrine;
  repoint to af-158, which states it verbatim.
- eval-marker parse (~2756): restore the POSITIONAL field-order rationale
  (.[0]=ts .[2]=round .[3]=win .[4]=run; every write site emits that order)
  into af-159.
- WORKDIR cleanup (~1584): restore the 'last step, after every reader
  including the artifact upload' ordering rule into af-148.

The two repoints are same-length, so qwen-autofix.yml stays 451750 bytes
(.size-baseline unchanged). Workflow meta-test suite 350/350 pass.

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-11-04 19:50:51 +08:00
a5fec03684 fix(ci): fail Java lanes before disk exhaustion (#16068)
* fix(ci): fail Java lanes before disk exhaustion

Co-authored-by: Qwen-Coder <[email protected]>

* fix(ci): gate the MySQL pool lane instead of a hosted job

The disk floor gate landed in hosted-harness-mysql, which runs on
ubuntu-latest, so `runner.environment == 'self-hosted'` is never true
there and the step was dead code. The job that actually shares the ECS
pool disk - mysql-integration, two Maven suites plus a per-leg MySQL
container - stayed ungated.

Move the step to mysql-integration, immediately after its checkout so
the sdk-java-workflow pin still holds. This also clears the three
Hosted real-process gates pins, which reject a conditioned step among
the setup steps that precede managed_dependencies in
hosted-harness-mysql, and reflow the new test to the repo's prettier
style.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvya43o17i

* style(ci): drop the blank line left behind in hosted-harness-mysql

Moving the disk floor gate out of hosted-harness-mysql left a stray blank
line between its checkout and 'Set up Java'. Restoring it makes the whole
workflow diff additive on mysql-integration only, so 'hosted-only jobs are
unchanged' holds byte for byte and an A/B census of that job reads clean.

Co-authored-by: Qwen-Coder <[email protected]>

* ci(sdk-java): record the real workflow size after the disk-floor gates

The six #16003 disk-floor gate steps grow sdk-java.yml from 154074 to
155650 bytes. main had already drifted 3742 bytes past the recorded
150332 — inside the 4096 allowance, but it left only 354 bytes of ratchet
headroom, so this real growth tripped Lint & Static.

Record the file's actual size rather than shrinking the gates: each one is
a three-line step calling .github/scripts/check-disk-floor.sh, and the
workflow test pins all six gated jobs.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw02ewzoa2

* fix(ci): derive the disk-floor corpus from runs-on and narrow the broker claim

Address the two blocking review findings on the Java disk-floor gate:

R1-1: the meta-test skipped any job without the gate and compared a closed
six-name list, so a newly pool-routed lane that forgot the gate stayed green,
and a neutered gate (continue-on-error) kept every assertion true. Derive the
corpus from each job's runs-on (same hazard as the setup-java corpus), require
gateIndex > -1 per pool-capable lane, and assert the gate carries no
continue-on-error. The derived set is the same six jobs today. Verified by
mutation: adding continue-on-error to a gate reddens it, and renaming a gate
(ungated pool job) reddens it with 'expected -1 to be greater than -1'.

R1-2: the step comment claimed RUNNER_TEMP covers the broker store, but the
absolute free-KiB floor cannot express the broker's used-ratio refusal (a
952 GiB volume at 94% used still has ample free bytes). Narrow the comment to
the gate's real scope (near-full filesystem / inode exhaustion, #10035) and
state the broker disk-threshold investigation stays out of scope, matching the
PR description. Bump .size-baseline for the comment change (155650 -> 155888).

sdk-java workflow meta-test 117 passed / 5 skipped; eslint exit 0, prettier
clean; check-workflow-size.sh passes.

* fix(ci): calibrate the Java disk floors and fail closed in the meta-test

Review round on this PR found the gate could not fire in the two shapes it
exists for, and that the meta-test could go green through the same drift it
was added to prevent.

R1-2: every lane ran the shared 2 GiB / 100k-inode default. The #16003
incident was a used-ratio refusal — the broker reported 0.94 while the
952 GiB volume still had tens of GiB free — so no absolute floor can express
that threshold, and 2 GiB is below what these lanes write: a host with 4 GiB
free passed the gate and then died mid-suite. Give the five heavy lanes the
repo's build-sized 8 GiB floor, leave flyway-migrations and the shared value
alone, and add an opt-in DISK_FLOOR_MAX_USED_PCT arm to check-disk-floor.sh
for the two broker lanes (90%).

R2-1/R2-2: the corpus came from `runs-on` text and skipped jobs outside it,
so a lane routed through an expression or a job output shipped ungated while
the suite stayed green, and nothing asserted the hosted-only jobs carry no
gate. Fail closed: an explicit hosted-only allowlist, a gate required for
every other job, a total-count assertion, and a negative assertion on those
two jobs. Pin the per-lane floors so dropping one is a test failure.

Verified: sdk-java-workflow suite 117 passed, 5 skipped; it goes red when a
gate is deleted, when one is added to a hosted job, and when a floor is
removed. check-disk-floor suite 12 passed, including four new ratio cases.

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: qwen-code patrol <[email protected]>
2026-11-04 17:57:04 +08:00
易良andQwen-Coder b383efb854 fix(ci): widen the built-tree download window for eight shards (#16285)
* fix(ci): widen the built-tree download window for eight shards

Eight shards now pull the same unit-test-tree artifact at once, so the
download cap has to clear the blob's tail latency rather than the median.
On main the slow host class took 4m52s, and two consecutive runs expired
twice at the five-minute cap before the unpack ran, which failed the
required Linux check with no test involved.

Raise both attempts to fifteen minutes and pin the pair in the
build-to-shard handoff contract.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(ci): size the shard download cap from the measured tail

Review flagged that the outgoing comment claimed a pool host had stalled
"for over a quarter of an hour", which the new fifteen-minute cap then
contradicts. That claim came in with the sharding commit and was never
tied to a run; neither incident can prove how far past five minutes the
tail went, so the comment now states only the measured distribution.

Also correct the number: the slowest completed shard took just under five
minutes, and it was a different shard that expired, on two consecutive
runs. Add the missing shard-side ceiling contract, mirroring the build
job's: every blocking step cap has to fire before the job cap, because a
cancelled job skips the failure-gated disk-pressure collector.

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-04 17:41:55 +08:00
易良 eefd4fd654 test(e2e): accept both plan-mode wordings in the ACP case (#16273)
`acp integration > blocks write tools in plan mode` filtered tool updates
with `includes('Plan mode')` and asserted `'Plan mode is active'`. The
unified agent loop blocks through the core scheduler, which says
`Tool blocked by plan mode: ...` (lowercase), so the case-sensitive filter
found no event and the nightly unified leg failed while every push leg
stayed green (run 41771749222).

Match `/plan mode/i` and keep the file assertion that proves nothing ran.

Verified locally: the case fails on origin/main with
`QWEN_CODE_ENABLE_UNIFIED_AGENT_LOOP=1` and the same message CI reports,
and passes both with and without that flag after the change.
2026-11-04 16:43:54 +08:00
易良 ad47896d85 test(interactive): share one CI-scaled wait policy across the TUI suites (#16276)
Nine interactive suites each declared their own polling helper with a fixed
budget (8s, 12s, 15s, 20s) and no allowance for where the suite runs. The
shared ECS pool runs several Actions runners per host, and hosted macOS VMs
are slower still, so an assertion that passes in ~12s locally expires
mid-repaint in CI. The current open reports all have this shape:
tui-foreground-shell (#15712, #15814, #16046), mid-turn-submit (#16250) and
mods-tool-middleware (#16040).

Move the helper to one module and scale its budget 3x when CI is set. A
stuck UI still fails on the same assertion, just later; no assertion or
product path changes. mid-turn-submit's 30s request wait gets the same
treatment: it exceeded that wait at 97s on a loaded host.

Verified locally: tui-foreground-shell, mid-turn-submit and
tui-editor-parity pass (33 passed, 1 skipped), eslint and the integration
typecheck are clean.
2026-11-04 16:43:29 +08:00
易良 d99bb685dd test(web-shell): wait for the restored card before clicking continue (#16286)
The nightly `web-shell Browser Regression` dies in the generated-UI
retention case at `expect.poll(() => text).toBeDefined()`: the click on
`#continue-analysis` produces no prompt request, so the capture route never
fires (run 41771749222, job 126382801964). The same case passes on a dev
machine — 6/6 runs here — so this is a host-timing shape, not a product
defect.

Wait for the card's continue control to be enabled, give dispatch a
CI-sized budget, and fail with the card and status state when nothing is
dispatched. The next nightly then names the cause instead of `undefined`.
2026-11-04 16:43:03 +08:00
易良 d24198e866 fix(ci): repin the RHEL 10 rpm root to the current stream10 digest (#16261)
The pinned manifest for the RHEL 10 build root no longer resolves on
quay.io: `quay.io/v2/centos/centos/manifests/sha256:99b8fe27…` answers 404,
so every job using that container dies at "Initialize containers" with
`manifest unknown` before a single step runs (run 41759710053, job
126345260531). The tag still exists, and resolves to
sha256:511d3584… today.

Repin to that digest. Staying on a digest keeps the build root fixed for the
life of the release, which is why this was pinned in the first place; only
the value moves.
2026-11-04 03:14:02 +00:00
e1bf0a2c24 fix(ci): resume linter archive downloads (#16067)
* fix(ci): resume linter archive downloads

Retry truncated actionlint and shellcheck downloads from the bytes already received instead of starting over on each attempt.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(ci): resume only after the first linter download attempt

The retry loop passed --continue-at - on every attempt, including the first
one, right after the stale local copy was removed. A resume request against a
missing file appends only the tail, so the archive failed its SHA-256 pin and
the lane died. The PR's own test asserts the opposite: the first request must
not resume, retries must.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvy7yxvq7f

* fix(ci): bound linter download retries to the CI step budget

Three review findings on the resume loop:

- Five attempts at --max-time 90 plus 5/10/15/20s of backoff is 500s,
  which outlasts the 5-minute 'GitHub CI helper checks' step that runs
  this installer (ci.yml:1433). On a stalled asset host the fourth
  attempt was cut off by the step, so neither the final exit nor the
  same step's --actionlint/--yamllint/helper tests ran. Three attempts
  put the worst case at 285s, in line with the ~273s bound of the
  curl --retry 2 call this loop replaced.

- curl resolves --continue-at - against a missing file to offset 0 and
  sends no Range header, so the old comment described a hazard that
  cannot happen and the first-attempt guard it justified was inert on
  the wire. The comment now states what the loop and the 33/36 branch
  actually do, and the guard is gone.

- Neither the 33/36 rm -f nor the attempt cap was covered: deleting the
  rm -f and raising the cap to 99 both left the suite green. Two tests
  now pin them, each verified to fail under its own single mutant.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvymz2cq81

* fix(ci): state the real resume budget and name the failed archive

Address the three review Suggestions on this PR's own new code:

- Budget comment: 285s is per archive, and the steps that provision both
  archives inside one 5-minute step can still cut the second one off.
  Also drop the PR body's "up to five attempts", which never matched the
  three the code ships.
- Terminal silence: the last attempt exits before the retry line, so a
  continue-on-error run ended on "retrying in 10s..." with no archive
  named and no exhausted budget. Echo that line, keeping curl's own
  exit status.
- Range-refusal rationale: measured curl exits 33 and leaves the
  partial byte-identical, so the rm -f is load-bearing because every
  attempt would otherwise re-send the same offset, not because a
  second body gets appended. The stub now models that sequence
  (28 with a partial, then 33 untouched, then a clean full body).

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw0d4pwgah

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-11-04 10:53:11 +08:00
易良andQwen-Coder 4593a3b8f2 fix(ci): run desktop smoke headless on rpm roots without Xvfb (#16211)
* fix(ci): give the RHEL 10 desktop smoke a headless display

RHEL 10 removed xorg-x11-server-Xvfb from every repository: BaseOS,
AppStream, CRB, extras and EPEL 10 contain no such build, so the rpm
build-root dependency step failed one step after #16154 fixed the EPEL
bootstrap (`No match for argument: xorg-x11-server-Xvfb`, run 41642580745,
job 125940965630).

Keep Xvfb for the Fedora 44 leg and give the RHEL 10 smoke a real display
instead: a weston headless compositor with rootless Xwayland on top, since
GTK3 has no headless backend and WebKitGTK always needs a display server.
Export WAYLAND_DISPLAY before Xwayland is launched, wait for both sockets
with a hard timeout, and pin the app to X11 so the bridge is actually used.

Verified end to end on a Desktop Checks dispatch of this head (run
41652880267): the RHEL 10 rpm lane passes dependency installation and the
packaged smoke, and the Fedora 44 lane keeps its xvfb-run path. The three
workflow meta-tests that parse this file pass (161 passed, 1 skipped).

* fix(ci): key the rpm display arm on the matrix and harden its meta-test

Review round 2 findings on #16211:

- R2-1: the display arm keyed on an artifact_suffix literal while the EPEL
  bootstrap it depends on keys on matrix.epel. Declare matrix.display
  ('xvfb' on Fedora 44, 'weston' on RHEL 10) and branch on that, so a leg
  added later picks a server instead of inheriting an arm by accident.
- R2-2: the xvfb-run arm shipped a shell function through declare -f into a
  child bash without -e -u -o pipefail. Run the same npm command in both
  arms so both keep the step's error regime; assert no declare -f remains.
- R2-3 / R1-4: bound the shared package list by the conditional that follows
  it instead of a prose comment, ignore comments when checking it, and assert
  the xvfb/weston arms, the weston socket name against WAYLAND_DISPLAY, and
  the Xwayland display number against DISPLAY and the X11 socket.

* fix(ci): fail the rpm display arm closed and pin the smoke's wiring

R2-1: the install arm keyed on `matrix.display` but defaulted every unknown
value to the weston arm, which only works where the EPEL bootstrap above has
run. Turn it into an explicit `case` with an error arm, and assert from the
matrix that a non-Fedora root taking the weston arm has EPEL enabled, so a new
leg with `display: 'weston'` and `epel: 'false'` turns the meta-test red
instead of dying at `dnf install` in a release run.

R1-4: restore the two pins the previous rewrite dropped
(`weston --backend=headless-backend.so`, `XDG_RUNTIME_DIR`) and bind the rest
to the step's structure rather than to bare substrings: the weston readiness
wait must sit between the two launches, the helper must `return 1` on timeout,
and the EXIT trap must reap both daemons. Each added assertion was checked
against the mutation it names (all five turn red; the unmutated tree stays
green).

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw1dkg4zbx

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-04 10:40:34 +08:00
易良 b5975157d1 fix(ci): drop the healthy-default apt gate and prefer the VPC intranet mirror (#16260)
* fix(ci): drop the healthy-default gate so a fast apt burst cannot veto the mirror

The merged probe (#16218) kept a third gate: override only when the default
measured below 250 kB/s. On the mainland hosts the deb.debian.org path is
bimodal — run 41743539611 on hz4-2 probed 372 kB/s (a fast 512 KiB burst), so
the gate kept the default, and the sustained 200 MB transfer then throttled to
45 kB/s and hit the 60-minute cap again.

Remove that gate. The two mirror-side gates already cover the cases it was
meant to protect: the 1 MB/s absolute floor rejects a "slow vs slower"
override, and the 4x factor keeps a genuinely fast default (the Hong Kong
hosts) on the built-in source. The hz4-2 sample (default 372 kB/s, mirror
1.97 MB/s) now selects the mirror; the Hong Kong and both-slow shapes are
unchanged. The contract test pins the floor, the factor, and the absence of
any healthy-default gate.

* fix(ci): prefer the VPC intranet apt mirror to bypass the capped public egress

The public mirrors still traverse the pool's capped 100 Mbps public egress that
throttles under concurrent load — the exact problem configure-maven-mirror.sh
solves for Central by preferring mirrors.cloud.aliyuncs.com, which answers from
inside the VPC and consumes no public bandwidth. Add it as the first apt probe
candidate. Off the VPC (hosted runners, Hong Kong hosts) it is unreachable, the
probe measures 0, and a reachable public mirror wins — so no host map is needed
and the probe self-verifies on each build.
2026-11-04 10:40:20 +08:00
易良andQwen-Coder 80953e053b test(integration): give the ACP parity rig a scratch runtime directory (#16224)
* test(integration): give the ACP parity rig a scratch runtime directory

TestRig points QWEN_RUNTIME_DIR at `<testDir>/.qwen-home`, and the parity
rig forwarded only QWEN_HOME to both ACP surfaces. Session runtime state
(`tmp/<hash>`, token usage) therefore landed inside the workspace, so the
folder-structure snapshot embedded in each request listed
`.qwen-home/tmp/<hash>` for the surface that inherited the rig's runtime and
not for the other: `expect(shared.requests).toEqual(current.requests)` then
failed whenever that ordering broke, and the retry went green (run
41668631944, first attempt failing three parity cases).

Point QWEN_RUNTIME_DIR at the same scratch home as QWEN_HOME so neither
surface writes into the workspace.

Verified with the fixed bundle: 24/24 parity cases pass. With KEEP_OUTPUT,
the test directory now holds `.qwen-home/settings.json` and
`trustedFolders.json` only, where origin/main additionally creates
`.qwen-home/tmp/<hash>` for the same case.

* test(integration): keep the ACP parity scratch runtime under KEEP_OUTPUT

Checkpoint for review suggestions R1-1/R1-2 on #16224. The parity rig now
keeps its scratch QWEN_HOME when KEEP_OUTPUT is set, matching
TestRig.cleanup()'s truthiness check, and pins the absence of the default
`<testDir>/.qwen-home/tmp` runtime directory directly instead of relying on
the request-snapshot diff.

Not verified locally: `npm run bundle` cannot resolve the generated
web-templates sources in this worktree, so the parity case was not executed.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw14zt15bk

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-04 08:00:28 +08:00
易良 aaf523cfc4 fix(ci): probe the apt mirror so mainland E2E hosts can build the sandbox image (#16218)
* fix(ci): probe the apt mirror so mainland E2E hosts can build the sandbox image

The docker E2E leg builds the sandbox image per push under a SHA-scoped tag, so
on any host whose image store no longer holds a recent build, the two apt-get
layers are downloaded again. deb.debian.org serves the Hong Kong hosts at MB/s
and the mainland-China hosts at 11-20 kB/s, which turns that build into 2-3
hours against the job's 60-minute cap. Over the past week 97 of 127 docker legs
on the ecs-qwen-hz* hosts were cancelled this way, and the run reports
`cancelled` instead of a verdict.

Probe rather than pin a mirror or a host-name map: the build measures
deb.debian.org and two domestic mirrors, and passes --build-arg APT_MIRROR only
when the default is slower than 250 kB/s and a mirror beats it by 4x. The image
keeps deb.debian.org as its default source, so release builds, local sandbox
builds and the healthy Hong Kong hosts are unchanged, and no fleet inventory
needs to be maintained.

The probe's measurements are logged, so the next slow build can be diagnosed
from its own output.

Refs #16214

* fix(ci): require an absolutely fast mirror before overriding the apt source

The relative guard alone only asked a mirror to be 4x faster than the default,
so on a host where both endpoints were slow the mirror still "won" and moved
the host onto a source it never needed. Require 1 MB/s in absolute terms too:
below that the build is slow either way, and the extra cache key is not worth
it.

* test(ci): pin the apt-probe threshold values, not just the gate structure

The new contract test asserted the gate variables and comparisons but not
their values: mutating slow_default 10x up or fast_mirror 10x down left the
suite green while flipping the both-slow host shape onto a still-slow mirror
(review finding on #16218). Pin both literals with a word boundary — a plain
substring pin would still match the 10x-up mutation — and witness both
mutations reddening the suite before restoring the values.

* test(ci): require the mirror ARG in both Dockerfile stages

A single-match assertion survived deleting the runtime-stage ARG APT_MIRROR
(R1-1, mutation-witnessed by review): the builder declaration satisfied the
regex while the runtime apt layer silently kept the slow default. Count both
declarations instead; deleting either stage's ARG now reddens the suite.
2026-11-04 05:52:30 +08:00
易良 b09e821485 test(integration): install the local extension fixture from a subdirectory (#16222)
TestRig.setup() now points QWEN_HOME at `<testDir>/.qwen-home`, and this
case installed `<testDir>` itself as the extension. An install copies its
source tree into the extension store, so the destination sat under the
source and fs.cp refused it: every E2E shard has failed on 35aa4b76 with
`Invalid src or dest: cp returned EINVAL (cannot copy ... to a subdirectory
of self ...)`.

Write the fixture into `local-extension/` and install that instead. The
case still covers install, list, update and uninstall of a local extension.

Verified against origin/main in a worktree: the case fails with the same
EINVAL before this change and passes (2/2) after it; eslint, prettier and
`tsc -p integration-tests/tsconfig.json` are clean.
2026-11-04 04:01:24 +08:00
易良andQwen-Coder d6b3e1fca7 fix(core): attribute monitor and workflow notifications to the registering turn (#16177)
* fix(core): attribute monitor and workflow notifications to the registering turn

Preserve the turn that starts monitors and workflow runs in their completion metadata so ACP sessions can consume results at the next tool batch instead of waiting for the turn to end.

Add registry coverage for attributed and unattributed registration, plus Session coverage for same-turn delivery and earlier-turn admission.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(core): keep caller turn and document same-turn injection scope

Address the three review suggestions on the registering-turn attribution:

- Prefer the invocation-context promptId over promptIdContext in both
  registries, and cover that arm: a daemon/ACP session holds different
  values in the two stores, so a fallback-only implementation would stamp
  results with an id the consumer never matches while staying green.
- Assign `sourceTurnId` with `??=` so a caller-supplied turn survives
  registration, mirroring the adjacent `todoWorkChainId` assignment, and
  pin that with a case per registry.
- Reword the design decision record in both language versions: same-turn
  injection applies to any notification whose origin turn equals the live
  turn, not only to subagents with origin metadata.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw08uebbab

* test(cli): cover bridge-context notification delivery

Pin monitor and workflow same-turn consumption under the invocation context used by daemon and ACP prompts, and align the paired design records with all background task kinds.

Co-authored-by: Qwen-Coder <[email protected]>

* test(cli): cover notification injection boundaries

Cover monitor and workflow delivery at a tool continuation, align monitor callback fixtures with the required event count, and pin unattributed results to automatic admission.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): repair type errors in the same-turn tool-continuation test

R3-2: the tool-continuation case added for the monitor/workflow notification
thread did not typecheck, breaking the packages/cli build:
- the execute mock was declared zero-argument but called with an AbortSignal;
  accept an optional signal parameter.
- sendMessageStream's message is PartListUnion (string | Part[]), so .some and
  .filter do not exist on the string branch; narrow with the file's existing
  'as Part[]' pattern (also resolves the implicit-any on the callback param).

Type-only change; tsc --noEmit clean for the file, same-turn suite 10/10,
eslint exit 0, prettier clean.

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-04 01:42:37 +08:00
易良andQwen-Coder 35aa4b76ae fix(core): require explicit trust for settings-file project hooks (#16196)
* fix(core): require explicit trust for settings-file project hooks

A repository .qwen/settings.json SessionStart hook ran at startup with
no trust prompt while the folder-trust feature was off by default:
getProjectHooks() consulted isTrustedFolder(), which returns trusted
when the feature is disabled. Gate it on isExplicitlyTrustedFolder()
instead, matching the frontmatter-hook and LSP gates from #16182.
Update the hooks docs, and grant explicit trust in hook test fixtures.

Refs #14746

Co-authored-by: Qwen-Coder <[email protected]>

* style(core): use Array<T> for the tuple-array cast

Co-authored-by: Qwen-Coder <[email protected]>

* test(integration): grant explicit trust in ACP parity fixtures

The ACP parity rig spawns the CLI with its own QWEN_HOME, so the trust
granted by TestRig.setup() never reaches those sessions. With project
hooks now requiring explicit folder trust, the UserPromptSubmit hook
scenarios silently lost their hooks. Seed the scenario home with folder
trust enabled and the rig directory trusted, matching the tool-hook
suites.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(core): align project-hook trust with the interactive boundary

Follow the Claude Code model: interactive sessions keep the explicit
folder-trust gate, while non-interactive invocations (-p, piped stdin,
ACP/SDK embeddings) treat trust as implicit — the caller chose the
directory and there is no prompt surface. This keeps platform embedders
working without trust-file migration. Surface a startup warning when
interactive project hooks are withheld so the gate is not silent, and
document the headless contract. Revert the integration-fixture trust
grants that the headless exemption makes unnecessary.

Refs #14746

Co-authored-by: Qwen-Coder <[email protected]>

* test(integration): drop ACP parity trust seeding made moot by headless exemption

The parity rig spawns non-interactive ACP sessions, which now treat
trust as implicit, so the seeded trustedFolders.json only perturbed the
request-snapshot comparison (an extra tmp/ tree under .qwen-home).
Restore the fixture to its main-branch shape; all 23 parity cases pass.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): tolerate partial settings shapes in the project-hook warning

The startup warning read settings.workspace.settings.hooks without
optional chaining; AppContainer tests render with a partial
LoadedSettings mock, and a missing workspace scope crashed the effect
before config initialization completed. Guard the lookup; all 272
AppContainer tests pass.

Co-authored-by: Qwen-Coder <[email protected]>

* docs(hooks): resolve trust-gate wording contradictions (R1-5)

Troubleshooting step 3 still stated the legacy trusted-folder rule and
the restart list omitted security.folderTrust and persisted trust
decisions. Align both with the interactive explicit-trust gate and the
headless implicit-trust contract, and mention the withheld-hooks
startup warning and browser state.

Co-authored-by: Qwen-Coder <[email protected]>

* test(integration): drop dead ACP trust seeding in the tool-hook suites

The per-session trustedFolders.json written beside the QWEN_HOME override
is never read: getTrustedFoldersPath() prefers the inherited
QWEN_CODE_TRUSTED_FOLDERS_PATH that TestRig.setup() stubs. Both blocks sit
in front of an --acp spawn, and ACP sessions are non-interactive, so the
headless exemption already loads project hooks without any trust grant.
Remove the writes and the now-unused realpathSync import, restoring both
suites to their base shape.

Refs #14746

* test(cli): pin the folderTrust derivation behind the project-hook gate

The interactive gate is covered by the Config table, but nothing pinned
settings.security.folderTrust.enabled -> Config.folderTrust. Assert that
loadCliConfig derives it and defaults it to false, so a derivation
flipping to true cannot silently arm repo-supplied hooks. Verified the
case goes red when the default is mutated to true.

Refs #14746

* docs(hooks): align trust-boundary descriptions with implementation

Remove the unimplemented claim that withheld project hooks appear in the hook browser. Clarify that loop.md retains the legacy folder-trust predicate while executable project hooks use explicit interactive trust.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): keep folder trust out of workspace reach

The project-hook gate reads security.folderTrust.enabled from merged
settings, so a repository could enable the very flag that authorizes its
own hooks and then reach any pre-existing TRUST_FOLDER or TRUST_PARENT
rule for the directory. Restrict the key to the operator scopes, beside
allowPrivateNetworkHooks, and pin both directions: a workspace can
neither enable the gate nor disarm a user's setting.

Also close the smaller review notes. The startup warning no longer
blames trust when disableAllHooks removed the hook system entirely, and
it now has coverage for the warned and both silent branches. Every hook
fixture ran pre-trusted, so add an interactive integration case that
declines trust and asserts the hooks never fire, the warning names the
remedy, and the tool result still reaches the model unmarked. Drop the
duplicated node:fs import and correct the docs claim that IDE trust
decisions apply live to settings-file hooks.

Refs #14746

Co-authored-by: Qwen-Coder <[email protected]>

* test(cli): align the daemon trust-grant case with the workspace restriction

security.folderTrust is now stripped from workspace scope, so a grant on
a workspace that only self-enables trust reports the disabled state
instead of refusing an ineffective grant. Keep the alias DO_NOT_TRUST
fixture: if the restriction regresses, the grant throws again and this
case goes red (mutation-verified).

Fixes the Test (ubuntu-latest) failure on a89935fa.

* test(cli): follow operator trust in the ancestor-trust case

R1-22 asserted that a workspace folderTrust value reaches the merged
settings, which is the behavior the workspace restriction removes. Drive
the case from the operator's flag instead: the merged value must survive
the repo's opposing declaration, and the ancestor provider must follow
the operator — denying every ancestor on the target's DO_NOT_TRUST rule
when the feature is on, falling back to directory safety when it is off.
Mutation-verified: dropping the restriction turns both cases red.

Fixes the Test (ubuntu-latest) failure on 6851f531.

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-03 23:54:00 +08:00
易良 e77f73e973 fix(vscode): keep the embedded-bundle guard failure a readable assertion (#16210)
When the guard fires on the ~32 MB embedded webview bundle, a failing
not.toContain makes vitest serialize a multi-megabyte diff; the worker
dies with RangeError: Invalid array length before the assertion can
render, and the Vite server close hangs. Check marker presence as a
boolean with a named message instead: same pass/fail contract, readable
AssertionError on failure. The small-bundle guard keeps not.toContain,
where a diff is cheap and useful.

Closes the last open finding on #10695 (ic:5503336042).
2026-11-03 22:50:13 +08:00
易良 dff7deb19c docs(release): explain hosted-runner queueing for publish (#16194)
* docs(release): explain hosted-runner queueing for publish

* docs(release): link the current release pool record
2026-11-03 22:41:31 +08:00
a8e6c2ee57 ci: scope triage finalize concurrency to PR runs (#16070)
* ci: scope triage finalize concurrency to PR runs

Co-authored-by: Qwen-Coder <[email protected]>

* test(qwen-triage-finalize): compare the concurrency group with whitespace flattened

The group is a folded YAML scalar, so its rendered value keeps a newline
next to the more-indented continuation line. Pinning the literal string made
the assertion fail on the PR's own workflow file (Test (ubuntu-latest,
Node 22.x) at d190b876c4), while GitHub Actions treats whitespace inside
${{ }} as insignificant.

Compare the flattened expression instead, matching the normalizer already
used for multi-line conditions in capture-tmux-ci.test.js. The workflow file
does not change.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvyeef987p

* test(qwen-triage-finalize): pin the folded group without normalizing

The more-indented continuation made the folded scalar keep a literal
newline, which the whitespace normalizer hid, so the assertion no longer
pinned the committed value. Put the expression on one physical line so it
folds to the single-space form, assert the parsed value directly, and
correct the comment that credited the wrap to the formatter.

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: qwen-code patrol <[email protected]>
2026-11-03 20:45:00 +08:00
易良 af318939bf fix(serve): address remote-access review findings R1-6/R1-7/R1-9/R1-11 (#16199)
Follow-up to #15887, fixing four review findings that were merged unresolved
(each labeled Suggestion but a real defect), with red->green regression tests:

- R1-6: the remote-access route catch-all echoed the raw internal error
  message (e.g. an absolute server path from a cert ENOENT) to the client and
  logged nothing. Now logs the stack to the daemon stderr and answers a
  generic message; RemoteAccessError keeps its intentional 409 code/message.
- R1-7: a successful status poll never cleared a prior poll/action error, so
  the role=alert banner stayed on screen indefinitely. accept() now clears it.
- R1-9: the polling effect was keyed on availability and reset the whole
  draft on every blip, wiping typed origins/port and the revealed token.
  Split the identity-scoped reset from polling so a transient availability
  blip preserves the operator draft.
- R1-11: the interface Select guard (!== 1) admitted zero interfaces and
  rendered an empty dropdown. Now renders only for >1 (0 shows noNetwork).

Witness: each new test fails without its fix and passes with it;
RemoteAccessSettingsCard 6/6, remote-access route 4/4. eslint + prettier clean.
2026-11-03 18:50:43 +08:00
易良andzzj 0f22726809 fix(core): honor Anthropic transport timeout configuration (#16193)
Co-authored-by: zzj <[email protected]>
2026-11-03 17:58:22 +08:00
1b689dd4be feat(serve): enable remote access from settings without restarting (#15887)
* feat(serve): enable remote access from settings without restarting

Add a locally managed, independently authenticated listener with live revocation and session continuity. Explain connection setup and reflect daemon availability in the sidebar.

Co-authored-by: Qwen-Coder <[email protected]>

* test(web-shell): expose isDaemonMixedContent in the boot-test daemon mock

Co-authored-by: Qwen-Coder <[email protected]>

* fix(serve): scope the Local Control pairing secret to its own listeners

`GET /workspace/local-control` returned the unredacted pairing URL and QR to
any bearer-authenticated request. The secondary listeners serve this same
Express app and each verifies only its own token, so a remote-access client
reached the Local Control pairing credential — a secret belonging to a
different listener, which disabling remote access never revokes.

Gate the unredacted branch on the listener that owns the credential (primary
or local-control) as well as on operator authority, and pin both arms: a
remote-access bearer gets `urlRedacted: true`, the owning listener still gets
the URL.

`POST /workspace/local-control/disable` stays open to remote-access callers.
That is now a stated decision rather than an inherited default: the bearer
already reaches every other route on this app, so denying it here would be
security theatre while breaking remote management, and the response is
redacted by the gate above.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvz8eo3a8v

* fix(cli): tear down a permission-blocked prompt at disconnect grace expiry

The preserve-running-prompts branch returned early for any live promptAbort,
including a prompt blocked on a client permission vote registered while the
stream was live. Once the remote entrance is revoked the vote can never
arrive (permissionResponseTimeoutMs defaults to 0), so the turn never
settled, promptSettled never ran, hasRunningDetachedPrompt stayed true, and
sweep plus both connection reapers skipped the connection forever — pinning
an ownedSessions entry and one of the maxConnections slots until restart.

Gate the preserve branch on hasPendingClientRequest: a session with an
outstanding permission vote falls through to the existing teardown, which
abandons the pending request and detaches the bridge client. A prompt that
is not waiting on the client is still preserved unchanged.

Witness: the new regression test fails without the condition (abandon
callback never fires: [] vs ['bridge-req-1']) and passes with it; the
existing 31-minute preserve test stays green (65/65).

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-11-03 17:16:50 +08:00
bc19325983 feat(web-shell): add host mount entries and align managed empty-state layout (#15928)
* feat(web-shell): 增加 managed-host 构建入口与托管会话页

为产品侧集成提供独立的 managed-host 产物(--mode managed-host)与托管会话页:
不挂 daemon workspace/session context,托管会话由 provider 直接渲染列表、会话与输入框。

包含:vite lib 构建模式、managed-host 渲染入口及测试、ManagedSessionsPage 托管分支与
测试、provider 类型调整、README 与中英文设计说明。本次不涉及宿主注入能力。

* feat(web-shell): 托管宿主可注入侧栏导航 props

ManagedAgentWebShell 增加可选导航 props 并原样透传给 ManagedSessionsPage:
navigationTarget / archivedNavigationTarget / onArchivedSessionsStatusChange /
navigationActive / navigationSearch。

宿主据此决定是否把官方会话列表 portal 到自己的侧栏(navigationTarget 为 null 时
仍走 home 布局但不 portal),并同步搜索词与归档计数。类型复用 ManagedSessionsPage
现有 props,不新增重复定义;新增 2 个用例覆盖原样透传与未传时保持 undefined。

* feat(web-shell): add full-page app-host entry and align managed empty-state layout

- Add app-host mount entry (renderWebShellApp) so a non-React host can render the
  complete WebShell (sidebar + welcome + composer) by injecting baseUrl/provider;
  no shell is hand-drawn and no backend address is hardcoded.
- Center the managed empty state like the standalone chat (content shrinks so the
  chatPane centers it) instead of pinning the welcome to the top and composer to
  the bottom; gate the welcome header off when a failed session is selected.
- Surface session failure and unconfirmed-submission notices together above the
  composer, dedupe the same message across channels, and style them as callouts.
- A pending prompt owned by another session now offers a redirect to the owning
  conversation instead of retry/discard from a foreign session; reconcile tests.
- Build app-host with global (unscoped) CSS so root-level state classes match.

* test(web-shell): align managed status smoke assertions

Co-authored-by: Qwen-Coder <[email protected]>

* test(web-shell): reach the owning conversation before retrying a foreign pending

A pending prompt owned by another conversation no longer offers retry in the
foreign session: it renders "Open the original conversation" instead. The
spec still clicked "Retry the same request" straight from the foreign session,
so the two foreign variants timed out waiting for a button that is deliberately
absent from this head while the owner variant passed.

Follow the redirect first, then retry. Owner variant unchanged.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvwo8rnm58

* docs(web-shell): document the app-host entry and drop ignored e2e links

The review on head 932a61d8dc read `app-host` as undocumented: no README
section, no mention in either design doc. Document it the same way
`managed-host` is documented -- entry API, build mode, served directory --
and record the unscoped-CSS behaviour as a deliberate decision narrowed to
that bundle, naming the entries that keep scoped CSS. Also state that neither
host mode is part of `npm run build`, which is what the wiring actually does.

Both design docs linked `.qwen/e2e-tests/managed-host-webshell.md`, which
`.gitignore` excludes (`.qwen/*`), so the links are dropped and the
statement they supported stays.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvxdyp0v69

* fix(web-shell): revert breaking changes to existing consumers

- Revert `isChatEmptyState` to `!managedMode &&` (was: `!connection.sessionId && !(managedAgentProvider !== undefined && managedSessionId)`), restoring the original behavior where managed mode is never empty state. The head condition broke existing managed consumers by making them hit empty-state layout when no session was selected.

- Revert `sidebarRailEnabled` to drop `managedAgentProvider === undefined &&`, restoring rail for existing managed consumers. The condition was app-host-specific and should not affect hosts that already pass a provider.

- Revert `ManagedSessionsPage` inline `flex: 0 0 auto` (was unconditional in empty state), restoring `flex: 1 1 auto` for the managed entry. The shrink was only correct when paired with `.appChatEmpty .chatPane { justify-content: center }`, which only exists in the full-page `renderWebShellApp` entry. In `renderManagedAgentWebShell` the class is absent, so the shrink alone produced a 136px content block pinned to the top with 550px dead space below.

- Remove `skipScope` from `vite.lib.config.ts`: the justification ("scoped selectors use [data-web-shell-root] as an ancestor anchor") is factually incorrect. `COMPONENT_SCOPE` includes `:where([data-web-shell-root][data-web-shell-shadcn], ...)` which matches the element itself, so scoped bundles already apply state classes on the root element. Keep scoping for all modes.

- Update README and both design docs to reflect the corrected scoping behavior.

Co-authored-by: Qwen-Coder <[email protected]>

* chore(web-shell): remove app-host entry

The app-host entry (renderWebShellApp) had no build wiring (absent from
build script, exports, files, and no CI job invoked --mode app-host), no
test coverage, and hardcoded a specific host's sidebar/footer configuration.
Remove it from this PR; it can be reintroduced in a follow-up with proper
packaging and tests once a named consumer exists.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(web-shell): drop unused mode parameter from injectCssModules

The mode parameter became unused after the skipScope removal. Remove it to
fix the ESLint no-unused-vars error.

Co-authored-by: Qwen-Coder <[email protected]>

* docs(web-shell): scope the managed-host entry and correct stale layout notes

- README: say plainly that `managed-host` is source-only (no `build`,
  `exports` or `files` wiring, so it is not in the npm package), and record
  that one shell per shadow root is the contract.
- package.json: keep the orphan `dist/types/managed-host.d.ts` out of the
  published tree so the package does not ship a type with no implementation.
- ManagedSessionsPage: point the `summary.failure` dedupe comment at the
  current failure block above the composer, and record why a session switch
  resets failure and discard notices.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvy1jgjo76

* docs(web-shell): state the real consequence of the session-switch reset

The comment claimed the list error is re-derived on a session switch. It is
not: the list effect's deps exclude sessionId, so a catalog-load error is
hidden until provider/listRevision changes. Say what actually happens.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvy1jgjo76

* fix(web-shell): keep managed sessions visible when workspaces collapse

Co-authored-by: Qwen-Coder <[email protected]>

* fix(web-shell): address doudouOUC review feedback

- Remove shadow-DOM style adoption from renderManagedAgentWebShell: the
  shared CSSStyleSheet cache caused cross-contamination when two shells
  shared one shadow root. Hosts using shadow DOM should adopt styles
  themselves or use a plain container.
- Scope the error/discard reset effect to hostSessionOnly only: the
  unconditional clear on [provider, sessionId] hid the catalog-load error
  for existing managed consumers on every session switch without retrying.
- Update the managed-host test to match the removed shadow-DOM behavior.
- Drop the stale shadow-root note from README.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(web-shell): preserve styles for managed hosts in shared shadow roots

Restore the documented managed-host ShadowRoot style lifecycle and keep the shared package sheet until its last owner unmounts. Cover both fallback and constructable sheet behavior.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(web-shell): center the managed host empty conversation as one group

The host mount entry has no .appChatEmpty ancestor, so the managed empty conversation kept its full-height content box and pinned the composer to the bottom. Give host-mounted empty conversations the same shrink-to-content centering, mount the entry through the managedHome fill wrapper, and drop the empty trailing status row below the composer.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(web-shell): let a host-mode session clear through without an echo channel

hostSessionOnly alone marks the shell controlled, so the page's clear
signal after deleting or archiving the current session was dropped when
the host passed no onSessionChange (the README-documented mount). The
deleted conversation stayed on screen and sendable.

Allow the internal write for a clear (undefined) when no echo channel
exists; new selections stay host-owned, preserving the existing
'unrequested selection is suppressed' contract. The bot's suggested
controlled-expression rewrite would have broken that contract, so the
gate lives in the handler instead.

Witness: the new no-echo test fails at 5683eed0 without this change
(sessionId stays 's1') and passes with it; the echo-channel test pins
that the fix does not leak internal writes into echo-owned hosts.

---------

Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-03 16:50:24 +08:00
易良andQwen-Coder 1f09497f45 fix(core): require explicit trust for project frontmatter and LSP (#16182)
* fix(core): require explicit trust for repository execution

* test(integration): explicitly trust skill hook fixtures

* test(lsp): add isExplicitlyTrustedFolder to document-sync mock config

The document-sync tests create a bare Config mock with only
isTrustedFolder. The new explicit-trust gate in NativeLspService and
LspServerManager calls isExplicitlyTrustedFolder(), which is undefined
on the mock, producing TypeError at reload time.

Add isExplicitlyTrustedFolder: () => true alongside the existing
isTrustedFolder mock so the 13 affected cases pass again.

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-03 15:44:31 +08:00
易良andQwen-Coder 31f64eb2e9 test(cli): pin same-turn shell notification delivery (#16176)
* test(cli): pin same-turn shell notification delivery

Co-authored-by: Qwen-Coder <[email protected]>

* style(cli): format the same-turn shell delivery test

Lint & Static failed on this head inside the incremental prettier step,
naming only Session.test.ts. Reflow the new `notifyShell` cast with the
repository's own prettier (3.6.1, matching the ^3.5.3 pin); the hunk is
whitespace only, ten lines re-wrapped, no assertion or call changed.

Co-authored-by: Qwen-Coder <[email protected]>

* test(cli): assert same-turn shell active-work hold

Shell holds are reported by category rather than by shell id. Move the check ahead of the delivery assertions so removing the consumed notification leaves the worker active and fails at the assertion meant to witness it.

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-03 14:41:58 +08:00
e4cf95e774 feat(config): add operator model catalog refresh switch (#16066)
* feat(config): add operator model catalog refresh switch

Honor a settings-level refresh switch from operator scopes, including bare mode, and skip the background models.dev request when usage statistics are disabled. Workspace settings cannot re-enable the refresh, and the existing environment-variable off switch still wins.

Co-authored-by: Qwen-Coder <[email protected]>

* style(config): make catalog refresh change set prettier-clean

`Lint & Static` failed in the incremental Prettier step (run 41303341242,
job 124838743484) on six files that this PR itself touches:

- docs/users/configuration/settings.md
- packages/core/src/config/config.ts
- packages/core/src/config/config.test.ts
- packages/cli/src/config/config.test.ts
- packages/cli/src/config/execution-sandbox-settings.test.ts
- packages/cli/src/config/settingsSchema.test.ts

Applied `prettier --write` with the repo config (prettier 3.6.1, the
version pinned in pnpm-lock.yaml). Formatting only: no behavior change.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvy5ts5g7c

* fix(cli): mirror modelCatalogRefreshEnabled in mcp reconnect

The throwaway Config built by qwen mcp reconnect passed usageStatisticsEnabled
but not modelCatalogRefreshEnabled, so the catalog-refresh gate defaulted to on
and the command still contacted https://models.dev/api.json even when the user
had model.catalogRefresh: false. The env var has no catalog equivalent, so only
the settings value is mirrored, matching loadCliConfig's resolution.

The reconnect suite passes at this head (38 passed).

* fix(config): keep the operator catalog refresh opt-out when a project replaces `model`

A trusted workspace that replaced the whole `model` section (`null`, `false`,
`[]`, or a string) left `stripSettingKeys` nothing to strip, and
`customDeepMerge` then assigned that value over the merged object. The
operator's `model.catalogRefresh: false` disappeared, `loadCliConfig` read
`undefined`, and core fell back to `true`, so a host that opted out of the
models.dev download fetched it anyway.

Restore the operator value in `mergeSettings` the same way
`tools.executionSandbox` already is, and state that the
`privacy.usageStatisticsEnabled` route is not workspace-restricted.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvzl9mtg9d

* fix(cli): carry the operator catalog refresh opt-out into the sandbox

The container mounts only the user settings dir and builds its env
passthrough from `process.env`, so a System-scope
`model.catalogRefresh: false` never reached the sandboxed CLI and it
still fetched models.dev. Set the refresh env var at the sandbox
handoff, which covers both the docker/podman passthrough and the
seatbelt child env.

Also move the operator scope reduce into a shared
`selectOperatorCatalogRefresh` beside `selectOperatorExecutionSandbox`
instead of keeping a second copy of the rule, and pin that a project
which replaced the whole `model` section keeps its own keys.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvzpjyde9j

* fix(cli): carry the usage-statistics opt-out into the sandbox handoff

Core refreshes the models.dev catalog only when both
`modelCatalogRefreshEnabled` and `usageStatisticsEnabled` are true, but the
sandbox handoff exported the opt-out for `model.catalogRefresh` alone. A
System/SystemDefaults-scope `privacy.usageStatisticsEnabled: false` is
neither mounted into the container nor forwarded, so the containerized CLI
still fetched models.dev despite the documented second opt-out.

Resolve the privacy switch with the same helper `loadCliConfig` uses, so the
env precedence matches, and export the opt-out when either half is off.

Tests: cover the usage-statistics direction and the "no opt-out" negative
case, and pin the `model.catalogRefresh` workspace strip by key so deleting
the restricted-list entry fails instead of removing an iteration.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvztu9wu9q

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-11-03 11:26:02 +08:00
276a447ef0 fix(ci): interleave PR review endpoint cohorts and report final routes (#16151)
* feat(ci): support same-model endpoint splits in PR review canary

The endpoint override derived the cohort by comparing the routed model
name against the configured canary/default models, so configuring the
SAME model on both cohorts — a pure quota split across two gateways —
labelled every PR 'default' and silently disabled the override.

Derive the cohort from the bucket predicate itself instead
(review_cohort_for_bucket: same clamp, same PR-number bucket test as the
model router), so model name equality no longer matters. The notice's
cohort field remains the attribution when both cohorts run one model.

* fix(ci): interleave review endpoint buckets and report final routes

Co-authored-by: Qwen-Coder <[email protected]>

* style(ci): apply Prettier to the review endpoint bucket test

The Lint & Static lane runs `node scripts/lint.js --prettier` on the
files a PR touches, and this PR's own test file was not formatted:
three `expect(...).toBe(...)` lines exceed the print width, so the
incremental lint step exited 1 on every head.

Formatting only - no assertion or fixture changed.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvzj4h0j9a

* fix(ci): share the endpoint host derivation and pin the cohort notice

- Extract openai_base_host() so the proxy bypass and the cohort notice
  derive the host from one place; the notice cannot name a host the
  traffic was not routed through.
- Drop the comments that still describe the removed model router.
- Pin the notice's bucket count (8% -> bucket 87/100), make the
  noKey/badUrl rows exact strings, and assert initial_openai_host so the
  three mutations that survived this suite now fail.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvznesli9g

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-11-03 02:20:46 +00:00
易良andQwen-Coder 5b7f9ec1c3 fix(ci): repair desktop RPM dependency and teardown checks (#16154)
* fix(ci): repair desktop rpm dependency and teardown checks

Co-authored-by: Qwen-Coder <[email protected]>

* test(desktop): use a counted space class in the EPEL checksum pin

`scripts/lint.js --eslint` runs on every file the PR touches, and the new
rpm dependency test spelled the two spaces in the pinned `echo` line as
literals: `no-regex-spaces` rejects them, so Lint & Static failed on the
incremental ESLint step.

`[0-9a-f]{64} {2}` matches the same two spaces the workflow emits; the
assertion still pins exactly what the release step writes.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvzj4h0j9a

* fix(desktop): keep the runtime group as a teardown target

Teardown collected daemon pids through the zombie-aware liveness predicate and
escalated SIGKILL only on leaders that were still alive, so a leader that had
already exited — reaped, replaced, or an unreaped zombie — silently dropped its
process group, which is teardown's only handle on it. The children the leader
spawned (MCP servers, tool subprocesses) then survived and kept writing under
the workspace about to be deleted, and the smoke reported a pass.

Collect every recorded pid, escalate on every collected group after the drain,
and keep the zombie-aware predicate on the "survived teardown" report so the
false positive this PR exists to fix stays fixed. The new fixture drives a
runtime whose leader exits while a writer in its group ignores SIGTERM.

Also pins the EPEL download URL and digest literals, drops the duplicate local
liveness predicate in favour of the shared helper, and covers the signal-probe
error arm (ESRCH gone; EPERM/EACCES alive, the rule the core helper documents).

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvzpjyde9j

* fix(desktop): decide teardown on the runtime group, not its leader

The round-2 escalation signals the recorded pid's group (`kill(-pid)`), but
the verdict and the drain still asked `isProcessAlive(pid)`, which answers
"gone" as soon as the leader exits — the exact topology
`SMOKE_FIXTURE_EXIT_AFTER_SPAWN` reproduces. In that state `waitForExit`
returned on its first poll, so the 5 s drain was skipped and the
`survived teardown` branch was unreachable.

`isTeardownTargetAlive` probes the group first and falls back to the leader
where there is no group to signal (win32), so the drain and the leak verdict
cover the children the group can still hold.

Also drops the "mirrors the canonical … keep both in sync" claim from the
desktop probe's comment: it diverges from core's `isPidAlive` on the
pid <= 0 guard, the zombie exclusion on the EPERM/EACCES arm, and the
vanished-/proc answer. The comment now states those differences and why
teardown wants the dead-on-ENOENT direction.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvzrp44z9n

* fix(desktop): stop counting unreaped zombies as a live teardown group

`waitForExit` asks `isTeardownTargetAlive` whether a daemon's group still
exists. `kill(-pid, 0)` also answers for members that can no longer write, so
a daemon that exited and that the container's PID 1 has not reaped kept its
group "alive": the drain burned its five-second window, the second probe still
returned the pid, and teardown reported "the packaged runtime survived
teardown" for a runtime that was already gone.

On Linux the group probe now counts only members that can still write: scan
`/proc` for entries whose pgrp is the recorded pid, and report the group
stopped when every member is a zombie. A scan that observes no member keeps
the group probe's answer, so a hidden /proc cannot end a drain early, and the
group handle still wins over the leader handle.

Measured on a real zombie group (setsid + _exit, parent never reaps): /proc
state Z, `kill(-pgid, 0)` exit 0, `isProcessAlive` false, and
`isTeardownTargetAlive` true before this change, false after it.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvzy4lf49w

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-03 10:18:16 +08:00
易良andQwen-Coder 4dce337ea2 feat(ci): let the PR review canary cohort carry its own endpoint (#16130)
* feat(ci): let the PR review canary cohort carry its own endpoint

The review canary split (QWEN_PR_REVIEW_MODEL_CANARY/PERCENT) only moves
the model name; both cohorts share REVIEW_OPENAI_BASE_URL and its key, so
a rate-limited canary model also burns the shared gateway's quota — the
shape behind the 429-storm review timeouts.

Add an optional endpoint pair for the canary cohort:
QWEN_PR_REVIEW_BASE_URL_CANARY (variable) + REVIEW_OPENAI_API_KEY_CANARY
(secret). When the bucket routes to the canary and both are set, base URL
and key move with the model and the canary host joins NO_PROXY. A URL
without its own key, or an unparseable URL, degrades to the default
cohort with a warning — forwarding the default key to another provider
would leak it. A canary model identical to the default model disables the
override (buckets are indistinguishable). The cohort notice now prints
the resolved cohort so rollouts stay attributable.

Unset, behavior is byte-for-byte today's: model-only split on the shared
endpoint.

* fix(ci): drop inert canary NO_PROXY append and record the workflow's new size

The canary endpoint override appended its host to NO_PROXY, but nothing in this
job consumes it: configure_qwen_network unsets HTTPS_PROXY/https_proxy/
HTTP_PROXY/http_proxy before the override runs, qwen does not consult NO_PROXY
when building its proxy agent, and the gh/git wrappers that do get the proxy
back only talk to github.com. Removing the two exports also removes the comment
that claimed a proxy-bypass symmetry the step cannot produce.

Lint & Static was red in "Check workflow file size": 276955 bytes against a
recorded 272192 with a 4096 allowance. The growth is the feature itself (the
env pair, the override block, the resolved-cohort field), not prose that could
move into a sibling file, so the baseline moves with the file rather than the
rationale being cut to fit.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvz69ico8s

* refactor(ci): simplify canary endpoint validation

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: qwen-code patrol <[email protected]>
2026-11-02 23:22:43 +08:00
易良andQwen-Coder b142071ab2 fix(core): report redirects on cd segments (#16129)
* fix(core): report redirects on cd segments

Preserve redirect-derived operations when resolving cd, pushd, and popd segments so Write rules see targets opened before the cwd changes.

Co-authored-by: Qwen-Coder <[email protected]>

* test(core): rename stale cd redirect test title

The title said 'ignores redirects attached to cd' but that is the
behavior this PR removes; the row survives only because >/dev/null is
filtered. Rename it to describe what it actually pins now.

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-02 22:03:14 +08:00
易良 c2921217ef ci: gate on the OS temp directory and shorten /tmp retention to 1d (#16126)
* ci: gate on the OS temp directory and shorten /tmp retention to 1d

The 2026-10-31 hz incident: /tmp tmpfs hit 100% inodes while the
workspace and RUNNER_TEMP sat on a data disk with millions free, so
every disk-floor gate was green and jobs died on mkdtemp ENOSPC
mid-suite. The gate now always checks the OS temp directory
(deduplicated against caller arguments); the tmpfiles policy drops
7d->1d because the measured 120-260k files/day steady state exceeds
the tmpfs inode ceiling at 7d. Mutation-verified: dropping the
os.tmpdir() check reds the suite.

* test(ci): cover the TMPDIR-unset default and pin the 1d retention

Review findings: the new OS-temp test never executed the production
incident path (TMPDIR unset -> /tmp default) — a stripped default
survived the suite green; and the 7d->1d retention change had no test
coverage — a revert shipped the incident again. Add the unset-TMPDIR
sub-run, a set-equality dedup assertion, and a qwen-docker-cleanup
suite pin on the exact tmpfiles line. Mutations verified red.

* test(ci): pin RUNNER_TEMP apart so the /tmp fallback test bites

The first version of the TMPDIR-unset test let the RUNNER_TEMP default
(/tmp) satisfy the assertion, so a stripped ${TMPDIR:-/tmp} default
survived green — the exact regression the test exists to catch. Pin
RUNNER_TEMP to a distinct directory; the /tmp sample can then only come
from the OS-temp append. Mutation now reds.

* test(ci): unset TMPDIR as empty string; destructure was a no-op

The previous fix still failed on clean trees: spreading a destructured
env cannot DELETE a key — process.env.TMPDIR re-entered via runGate's
own spread, so the /tmp fallback never fired and the test failed
against the correct script. An empty-string TMPDIR is bash-unset for
${TMPDIR:-/tmp} (and bare for the regression), which is the exact
distinction under test. Clean 8/8; stripped-default mutation reds.

* ci(ecs-runner): enforce the /tmp retention from the owned daily service

Review finding: the 1d policy's enforcement agent was the distro's
systemd-tmpfiles-clean.timer, which can be masked on hardened images and
reports 'static' (unverifiable via is-enabled) on stock Ubuntu — with
nothing in the qwen stack running systemd-tmpfiles --clean, the measured
120-260k files/day steady state regrows past the tmpfs inode ceiling and
the 2026-10-31 incident class recurs with every test green. Add
ExecStartPost=systemd-tmpfiles --clean --prefix=/tmp to the daily
qwen-docker-cleanup.service, pin it in its test suite, and document the
enforcement owner in the README verification block. Mutation-verified.
2026-11-02 22:02:36 +08:00
a983503f02 fix(core): attribute shell notifications to the registering turn (#16125)
A background shell that settles mid-turn never reached the model until the
turn ended: ShellNotificationMeta carried no sourceTurnId, so
Session.#takeCurrentTurnBackgroundParts — which skips queued items whose
sourceTurnId differs from the current turn — could never match the item,
and delivery fell through to the end-of-turn drain.

Capture the registering turn in BackgroundShellRegistry.register(),
mirroring BackgroundTaskRegistry (getInvocationContext()?.promptId ??
promptIdContext.getStore()), declare it on ShellTask, and forward it
through ShellNotificationMeta into the Session shell callback. Both the
direct is_background: true spawn and the foreground→background promote
funnel through register(), so one assignment covers both; a promoted
shell is attributed to the promoting turn, which is the turn that ends up
waiting for the result.

Monitor and workflow callbacks have the same gap; left as a follow-up.

Guarded by two registry tests: the completion meta carries the
registering turn, and a shell registered with no turn context stays
unattributed — and still emits — rather than inventing an attribution.


Patrol-Run: qwen-issue-patrol/jmvz3emo2tv

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-02 22:02:00 +08:00
易良andQwen-Coder e7b86ae32d fix(mcp): warn about disabled tool typos on every discovery path (#16120)
Refs #16078

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-02 21:41:17 +08:00
38b71299c8 feat(desktop): build and publish rpm installers (#16060)
* feat(desktop): build and publish rpm installers

Add Fedora 44 and RHEL 10 release roots, strict RPM smoke checks, and artifact naming that keeps each root distinct.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(desktop): declare rpm runtime requirements and clear the yaml gate

The rpm legs built artifacts with no `Requires:` entries: Tauri's rpm bundler
writes them only from `bundle.linux.rpm.depends`, which nothing set, so
`verifyRpmDependencies` had no webkit soname and no glibc floor to find and the
release could not pass its own gate.

- Add `rpm-fedora44.conf.json` / `rpm-rhel10.conf.json` overlays declaring the
  webkit soname and each root's glibc floor (2.43 vs 2.39 cannot share one
  config), and pass the per-leg overlay to `tauri build --config`.
- Quote-free `container: ${{ matrix.container || null }}` stays as is with an
  inline yamllint disable: quoting the expression would turn a null container
  into an empty string. The shape test pinning that literal moves with it.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvy1jgjo76

* ci(desktop): record the rpm legs in the workflow size baseline

`desktop-release.yml` is now 49858 bytes, past its recorded 45089 plus the
4096 allowance, so `Lint & Static` fails on the ratchet before any linter runs.
The growth is the two rpm legs: their matrix entries, the per-leg overlay
wiring, and the roots' comments. Moving that prose out would not pay for it —
the legs themselves are the bytes — so the number moves with the file in the
same PR, as the gate asks.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvy1jgjo76

* style(desktop): apply prettier to the rpm legs and realign the workflow shape pin

Prettier flagged .github/workflows/desktop-release.yml and
packages/desktop/scripts/smoke-linux-installers.js on head 10acaadeba.
test-release.js pinned that container line with two spaces before the inline
yamllint disable comment, which prettier rewrites to one; .yamllint.yml has
min-spaces-from-content: 1, so the pin now follows the formatter instead of
blocking it.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvy3ombr79

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-11-02 20:50:05 +08:00
易良andQwen-Coder 8aea64d5f7 fix(desktop): verify AppImage library closure (#16062)
Read every ELF's DT_NEEDED entries from the extracted AppImage and fail release when a dependency is neither bundled nor part of AppImage's host-provided baseline. Document the contract and cover the failing and repaired cases.

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-02 19:43:23 +08:00
易良andQwen-Coder 5df87f7080 fix(desktop): stop the bundled runtime from the Windows installer (#16071)
An update started by a desktop build released before the app-side runtime stop can still leave the bundled Node daemon holding node.exe while NSIS replaces the runtime. Clear that lock from the installer pre-install hook by image path, wait boundedly for the image to become writable, and abort before copying unless PowerShell returns an explicit success. Pin the behavior in the desktop release gate.

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-02 19:40:13 +08:00
易良andQwen-Coder 292c0da525 test(install): pin Windows PATH persistence contract (#16072)
* test(install): pin Windows PATH persistence contract

Co-authored-by: Qwen-Coder <[email protected]>

* test(install): format the pinned PATH assertion

Prettier collapses the single-string expect(...).toContain(...) call at 76
columns, which the incremental lint gate rejected on the previous head.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvygjkzk7s

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-02 15:44:05 +08:00
易良andQwen-Coder 73a7268447 test(cli): guard settings reference against schema drift (#16085)
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-02 15:29:30 +08:00
0ebbe15221 fix(review): name the model half in the anchor re-run and drop the retired range model (#16094)
* fix(review): name the model half in the anchor re-run and drop the retired range model

F1 (rc:3808708929): the marker-recovery re-run sentence named only
`--since <sha>`, while the cache path two paragraphs above requires the
pair `--since <lastCommitSha> --since-model <lastModelId>`. A reader who
applied just the re-run sentence handed `fetch-pr` a sha with no model
half, which is exactly the anchor the same-model gate exists to rule on.
The sentence now carries `--since <sha> --since-model <model>` and says
the model half rides the same replacement rule, so the existing
"REPLACING any --since it already carries, never appending a second one"
semantics are unchanged.

F2 (rc:3810474869): the `effective: true` bullet still called the
report's diff and plan `since..head` — the full-range model the
incremental narrowing retired. It now names the sliced scope the report
publishes (`deltaFiles`, widened by `interaction[]`), consistent with the
`incremental.scope` wording in the same bullet.

Both are pinned by new assertions in SKILL.test.ts: the re-run sentence
is sliced to itself before asking for `--since-model` (the same words
occur in the cache path, where they were already correct), and the
`effective: true` bullet is sliced to the next bullet and asserted free
of `since..head` while still naming the three scope classes.

Refs #9654

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-issue-patrol/jmvyftv3bt8

* docs(review): take both anchor values from the side file on the re-run

The marker-recovery re-run said to pass "exactly as the command above
carries it", but on this path that command carries no --since-model (no
cache) or another model's (cache anchor withheld) - so a literal reader
was refused as cross-model-anchor and reviewed the full diff, the very
failure the recovery path exists to remove.

Both values now come from the side file's sha/model pair, and the
replacement rule names both flags. The adjacent pin moves with the
sentence in the same commit.

Co-authored-by: Qwen-Coder <[email protected]>

Patrol-Run: qwen-pr-closeout/jmvyioqrq7v

---------

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-02 15:29:15 +08:00
易良andQwen-Coder 91bce47a14 fix(vscode): bucket session history by calendar week (#16097)
The session history dropdown grouped entries with a rolling 168-hour
window while the buckets are labelled Today / Yesterday / This Week /
Older. On a Monday, a session from the previous Saturday was still within
168 hours, so it showed up under "This Week" even though the calendar week
had only just started.

This Week now begins at the Monday-first start of the current calendar
week, derived from the same local day boundary as the Today/Yesterday
buckets. The boundary uses setDate rather than millisecond arithmetic so a
DST transition cannot shift it. Sessions before that boundary fall through
to Older, and Today/Yesterday are untouched.

Regression coverage pins a Monday with a previous-Saturday session (Older),
plus a Friday/Wednesday positive control proving This Week still exists
mid-week so the Monday assertion cannot pass on a missing group.


Patrol-Run: qwen-issue-patrol/jmvyk46lrtc

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-02 15:28:27 +08:00
易良andyiliang114 4700dd2368 fix(web-shell): put the list-failure notice under the tabs, not above them (#16104)
* fix(web-shell): put the list-failure notice under the tabs, not above them

#16082 merged with the notice as the nav's first child, so a failed session load
rendered above the Sessions/Archived tab bar and pushed the list's own controls
down - an error about the list appearing before the list's navigation. It now
renders after the tabs and the title filter, in the region the sessions occupy.

Also stops break-all from cutting the request id mid-token ('req' /
'uest-list-500'); break-words keeps it whole and wraps at a boundary.

Observed in a real browser at 1280x800 against a 500 from /sessions/query:
  before: nav children = [alert y=52, tabs y=180, search y=212]
  after:  nav children = [tabs y=52, search y=84, alert y=128]

The file also carried two prettier violations from the squash merge; prettier
--write normalises them, which is why the diff touches two unrelated indents.

* style(web-shell): keep the diff to the move and the word break

The previous commit ran a newer prettier than this repo pins, which reindented two
unrelated call sites and does not satisfy the repo's own --check. Redone from the
pristine file with the pinned prettier (3.6.1): the net diff against main is now
only the block move and break-all -> break-words.

---------

Co-authored-by: yiliang114 <[email protected]>
2026-11-02 12:50:12 +08:00
9dde2f657d fix(web-shell): surface Managed session list failures (#16082)
* fix(web-shell): surface Managed session list failures

Co-authored-by: Qwen-Coder <[email protected]>

* test(web-shell): match the list-failure sentences, not the whole alert

The list-failure alert wraps its message and its Retry button in the same
role=alert div, so textContent reads 'Managed sessions could not be loaded.Retry
loading sessions'. Two assertions compared that concatenation against the
sentence alone, which made four cases fail with a message that looked identical
on both sides.

Assert the sentence is present, the way the sibling HTTP-500 case already does,
and keep the alert count so the 'once' property is still pinned.

Not verified locally: this checkout's core dist predates
managed-runtime/managed-permission-rules.js, so the suite cannot load here. CI
is the first real run.

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-11-02 10:41:58 +08:00
dd020ddfcc fix(web-shell): sanitise goal approval text and edit paths (#16080)
* fix(web-shell): sanitise goal approval text and edit paths

Co-authored-by: Qwen-Coder <[email protected]>

* style(web-shell): let Prettier place the diff-path chain

The Lint & Static gate runs prettier --check and this test file failed it on a
member chain that wraps differently. Formatting only.

Not fixed here: neutralises controls in the edit diff path still fails on this
branch (expected '+1-1' to be the sanitised path), on the unformatted original as
well, so it predates this commit.

* test(web-shell): reach the diff path past DiffView's own wrapper

The case asserts that a crafted edit path has its zero-width joiner, ANSI escape
and BEL rendered as visible escapes. It walked from the File diff region up one
level and took the first child, which is DiffView's stats element - hence the
'+1-1' the assertion reported. The path sits one level higher, as the sibling of
DiffView's root, so the traversal needs one more parent.

Selector only; the assertion and the sanitisation it exercises are unchanged.

* fix(web-shell): tolerate a diff block without a path

DiffBlock.path is optional, but the new sanitise call passed it straight to
sanitizeControlChars, which takes a string. tsc -p tsconfig.lib.json rejected
it as TS2345 at ToolApproval.tsx:727, which is what took down Lint & Static,
Test (ubuntu), Integration Tests, web-shell E2E and Capture visuals on the same
head. Use the file's existing optional-string shape ("?? ''") so a path-less
block still renders an empty label, as before.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvy7yxvq7f

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-11-02 06:16:31 +08:00
易良andQwen-Coder dfeae12de0 fix(core): use the amend delta for loosely spelled amend commits (#16073)
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-02 00:43:20 +08:00
1ac4f6cd1b fix(sandbox): install fonts-dejavu-core in the runtime image (#16032)
The capture card renderer in scripts/verify-capture.mjs requests DejaVu Sans Mono, but the runtime image installs no font packages at all.


Patrol-Run: qwen-issue-patrol/jmvx4obxsrz

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-11-01 10:53:37 +00:00
易良andyiliang114 7f774219ae test(opentui): run the Windows preview installer suite through cmd.exe (#16028)
* test(opentui): launch the Windows preview entry through cmd.exe

The PowerShell preview installer preservation suite failed on every
nightly Windows run: the seeded qwen.cmd launcher printed nothing, and
the leftover batch child kept the file locked so teardown hit EBUSY.

PowerShell was only a way to run a .cmd from Node. Its `&` call plus
`exit $LASTEXITCODE` returned before the batch child wrote anything, so
run the entry through cmd.exe instead (`shell: true`).

PowerShell also writes its own startup cache under
%LOCALAPPDATA%\Microsoft\Windows\PowerShell. The fixture pointed
USERPROFILE at the home tree the assertions treat as installer-owned, so
every successful test failed on that cache. Point APPDATA and
LOCALAPPDATA outside the compared home.

* test(opentui): compare home files, not shell-created folders

With the launcher fixed, every case in the suite now reaches the
preservation assertion and fails on Windows because PowerShell creates
its own empty %USERPROFILE%\AppData folder shells on first run. Those
are the shell's account directories, not installer output.

Redirect APPDATA/LOCALAPPDATA outside the fixture home (so the startup
cache file stays out too) and compare only the home's file contents.

---------

Co-authored-by: yiliang114 <[email protected]>
2026-11-01 10:53:33 +00:00
易良 eaafba5d14 ci: route the classify_pr gate to ECS for every pull request (#16019)
The gate is the needs-prerequisite of every Linux lane, but its own
runner stayed association-gated: untrusted fork PRs queued the whole
chain on the scarce hosted pool (observed 30+ min on PR #15851).
classify executes only trusted base-SHA code with a read-only token —
a strict subset of the lint exposure accepted on 2026-10-31 — so route
it unconditionally to ECS, kill-switch excepted. The ubuntu_runner
association gate for the test/integration lanes is unchanged.

Routing battery: gate matrix across all associations, structural pin
against clause creep; association-clause mutation reds the suite.
2026-11-01 00:12:22 +08:00
cca888dfcc fix(core): bump MCP client and core to 2.4.0 to drop pre-bundled fast-uri 3.1.0 (#15925)
* fix(core): bump MCP client and core to 2.4.0 to drop pre-bundled fast-uri 3.1.0

The MCP client 2.0.0 pre-bundles ajv with an inlined fast-uri 3.1.0,
which dependency audits cannot see and overrides cannot reach. 2.4.0
re-bundles fast-uri 3.1.8, clearing CVE-2026-6321, CVE-2026-6322,
CVE-2026-18446, CVE-2026-75975 and CVE-2026-76172 from the published
bundle. Both packages stay exact-pinned in lockstep because the client
pins core to the same version.

Fixes #15919

Co-authored-by: Qwen-Coder <[email protected]>

* chore(vscode-ide-companion): regenerate NOTICES for MCP client/core 2.4.0

Co-authored-by: Qwen-Coder <[email protected]>

* fix(core): refuse redirects before the SDK, not only inside the fetch

2.4.0 resolves a same-origin 30x in its own layer, against
requestInit.redirect, before the compatibility fetch is called. The Agent
Plugin transport only set headers, so a configured or bearer credential was
replayed to another path on the same origin.

Ask the same question in one place and carry the answer into requestInit on
both transport paths, and pin it with a test that fails when the option is
absent.

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-10-31 19:25:36 +08:00
1886aa7221 test(ci): pin the supersede kill-dir prefix to the workflow's (#15963)
The supersede kill dir is minted under RUNNER_TEMP with a fixed prefix and
never exported, so the replay's forge stub can only find it by listing that
prefix. The literal was written twice — once in the workflow, once in the
stub glob — and renaming one side alone left the stub globbing nothing: the
forged kill record never landed and the refusal replay still passed without
exercising the refusal.

Derive the production prefix from the workflow text at module scope and pin
the stub glob to it, so a one-sided rename is red instead of vacuous green.
The stub case line now consumes that shared fixture string.


Patrol-Run: qwen-issue-patrol/jmvvrdn39qo

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-10-31 18:00:45 +08:00
8ca595c687 fix(vscode): gate the companion vitest worker cap on the shared pool (#15940)
* fix(vscode): gate the companion vitest worker cap on the shared pool

packages/vscode-ide-companion was the one guarded config that raised its
test ceilings for the ECS pool without touching worker count. Every
sibling (core, cli) pairs the timeout ternary with a maxWorkers one, so a
25%-of-cores vitest run on a host carrying several concurrent jobs was
the only thing keeping this workspace from adding to the oversubscription
it was already widening its timeouts for.

Add the same ternary core carries — '25%' on `ecs-qwen-*`, undefined
otherwise — so the cap exists only where the contention does.

scripts/tests/unit-vitest-configs.test.ts pins it from both sides in the
autofix gate clamps case: the ECS arm now reads the companion config
alongside core/cli and expects the gate's own --maxWorkers value, and a
second, separately re-imported arm with RUNNER_NAME unset expects
`maxWorkers` to be undefined. The off-pool arm is what makes the pin
load-bearing: a flattened `maxWorkers: '25%'` passes every pool-lane
assertion, and only the second import round sees it (mutation-checked).

Refs #10695

Co-authored-by: Qwen-Coder<[email protected]>
Patrol-Run: qwen-issue-patrol/jmvva8cvtq7

* test(vscode): correct the config-clamp comments the reviewer disproved

The reviewer was right on both statements I added: the autofix gate does not
launch vscode-ide-companion, and normal CI does not run this workspace
unlimited. run-unit-test-lanes.sh exports VITEST_MAX_THREADS (halved for the
side lane, 2 where unset) and vitest resolves
poolOptions.threads.maxThreads before config.maxWorkers, so the config value
is a default for runs carrying no such env, not the lane ceiling.

Comments only; the clamp and its pins are unchanged.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvw0o04b4b

* test(vscode): move the companion worker-cap pin to the bundle-guard block

The review disproved the clamp-array coupling: the gate passes its
clamps on the command line for whatever changed packages it launches,
so the companion config does not need to track that array. The
bundle-guard block already re-imports this config per runner name;
extend it with the maxWorkers expectation (ECS '25%', foreign and
unset undefined) instead of a second stub/reset/import round in the
clamp test. The unset arm keeps the stronger mutation coverage.

Clamp block restored to its pre-PR membership and comment.

---------

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-10-31 17:50:28 +08:00
易良andyiliang114 54d47667d2 refactor(review): build the all-zero DriveReport through one factory (#15943)
`runDrive` returned the same all-zero report — `unavailable`, nothing
observed, no exit code, empty output — from four separate exits, each a
hand-copied literal that differed only in its `note`. A field added to
`DriveReport` later would land in some of those copies and not others.

Add `notStartedReport(note)` and route those four exits through it. The
two exits that DID start something — the readiness timeout and a failed
`new-session` — carry live `readyAfterMs` / `killedStale` and keep their
own reports, unchanged.

Refs #9695


Patrol-Run: qwen-issue-patrol/jmvvcdimeq9

Co-authored-by: yiliang114 <[email protected]>
2026-10-31 17:24:30 +08:00
5af73b6f09 fix(review): route isAoneHostFamily through the shared host normalizer (#15934)
* fix(review): route isAoneHostFamily through the shared host normalizer

`isAoneHostFamily` re-inlined the exact normalization chain
(`toLowerCase()` + strip `:\d+$` + strip trailing dot) that
`normalizeHostSpelling` already owns, so the helper's contract comment —
"both host predicates route through it" — was false for the third
predicate. The two copies were character-for-character equivalent, so
behaviour was identical; this is a consistency defect, not a live bug.

Route the family predicate through `normalizeHostSpelling`, leaving the
helper, the canonical write router and the `.alibaba-inc.com` wildcard
untouched.

Covered by a new `isAoneHostFamily` describe block; the paired mutation
witness (helper returns `host + '.invalid'`) leaves those assertions
green before the change and turns them red after, while the
hostsEquivalent/isAoneCanonicalHost control legs fail in both runs.

Refs #9601 (finding rc:3820959694)

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-issue-patrol/jmvv8372lq5

* style(review): quote the new isAoneHostFamily test name per prettier

The family-predicate case added in ce55ef2972 used a double-quoted `it`
title, which fails `node scripts/lint.js --prettier` (repo config is
singleQuote). Behaviour unchanged: title text only.

Patrol-Run: qwen-pr-closeout/jmvv8sx0037
Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-10-31 17:23:45 +08:00
ca7d487ce9 ci: route Lint & Static to ECS for every pull request (#15974)
* ci: route Lint & Static to ECS for every pull request

* ci: isolate HOME and complete routing pins for lint_and_static

- lint_and_static admits untrusted forks to the shared pool, so pin a
  job-level HOME/USERPROFILE under runner.temp and create it before the
  first write (git safe.directory, npm config).
- Derive the classifier output from each job's own runs-on in the ceiling
  helper, so a timeout pointed at the wrong output throws instead of
  being substituted; parameterise runnerOn the same way.
- Pin test's own runs-on to ubuntu_runner and add the HOME-isolation
  guard for every lane routed past the association gate.
- Correct the qwen-autofix premise and the integration_no_ak comment,
  and record the routing decision in a bilingual design note.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvvwdojb45

* fix(ci): isolate lint lane HOME and restore routing guard strength

Resolve R1-1 (Critical): per-job HOME for lint_and_static so untrusted
fork code cannot plant git/npm config a later trusted job inherits;
shared warm caches re-pointed via getent + XDG_DATA_HOME/COREPACK_HOME.
Resolve R1-2: timeout ceilings derive the output name from each job's
own runs-on. Resolve R1-3: explicit ubuntu_runner pin for test.

* docs(ci): record lint ECS fork-routing design and correct stale premises

Resolve R1-5: bilingual design record (container tradeoff, acceptance,
residual risks), qwen-autofix.md section 38 premise, no_ak scoping comment.

* fix(ci): publish run-unique isolated HOME from step env, not job env

Resolve P1/R1-1: jobs.<job_id>.env cannot read the runner context, so the
workflow failed GitHub validation entirely; publish HOME/USERPROFILE via
GITHUB_ENV from the first step instead. Resolve R2-1: RUNNER_TEMP survives
across jobs on this pool, so the path is run-unique, cleared before create,
and reclaimed by an always() step before the failure collector. Resolve
R2-3: share content-verified linter/corepack caches via RUNNER_TOOL_CACHE.
Resolve R2-5: move the HOME guard into the PR-gated routing battery with a
single-lane set pin; resolve wenshao R2-4 with one fleet ladder for both
outputs. Witness: actionlint 1.7.12 exits clean on ci.yml; six mutations
each red the matching guard.

* docs(ci): correct lint routing record per round-2 review

Supersede link points at qwen-autofix.md section 38 (R2-4); read-only token
and HOME-relative write class stated precisely (R2-2); residual risks are
examples not a census, naming web_shell_e2e_smoke and other PAT pool jobs
(wenshao R2-3); association gate recorded as throughput policy, not an
enforceable boundary (R2-6); dispatch-over-fleet priority and per-lane
hosted ceilings (P3); cache cost list and PR-gated test placement updated.

* chore(ci): bump ci.yml size ratchet for merged tmpdir routing and HOME isolation

Growth is real: #15979's helper-test tmpdir routing arrived via the main
merge the freshness gate required, plus this PR's run-unique HOME
mechanism. 156377 -> 161281 bytes.

* fix(ci): keep COREPACK_HOME per-run; shared cache is a poisoning channel

Corepack does not re-verify extracted entries on cache hit (reproduced
locally on 0.36.0 with the repo's pnpm pin: tampered entry executes with
the recorded hash unchanged, network disabled). Move COREPACK_HOME inside
the run-unique HOME; XDG_CACHE_HOME stays shared because lint.js
SHA-256-verifies linter archives on every hit. Guard and bilingual record
updated; shared-corepack mutation reds the guard.

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-10-31 16:35:58 +08:00
b3c392235a fix(core): keep session-commit registration for an absolute cd into the same repo (#15953)
* fix(core): keep session-commit registration for an absolute cd into the same repo

`cdTargetMayChangeRepo` returned true for ANY target starting with '/',
with no reference to the shell's cwd. The product-recommended spelling
`cd /abs/path/to/this/repo && git commit -m "..."` therefore latched
`cwdShifted`, `attributableInCwd` went false, `trackSessionCommit` never
ran, `sessionCommitShas` stayed empty, and the agent's own next
`git commit --amend` was hard-blocked by the amend guard.

Resolve an absolute target against the shell cwd and treat the cwd itself
or a subdirectory of it as in-repo. Sibling and out-of-tree targets still
shift, including the `<repo>-sibling` case that a bare `startsWith` would
wrongly admit. Relative targets are untouched, and resolution stays static
(`path.resolve`) — no `git rev-parse` in the walk.

`gitCommitContext`, `isAmendCommit` and `findAttributableCommitSegment`
now take the cwd; all four module-private call sites thread it through.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-issue-patrol/jmvvn3bibqk

* fix(core): refuse an absolute `cd` carrying an unresolved shell reference

The containment test for an absolute `cd` target resolves statically, so
`cd /repo/$PKG` sits lexically under `cwd` while bash lands wherever
`$PKG` expands. The absolute branch returned before the env-var guard, so
that spelling was certified as in-cwd and the Co-authored-by trailer was
spliced onto a commit in another repository — a regression against the
pre-#12514 `target.startsWith('/') -> true` short-circuit.

Check `$`/backtick before the absolute branch, and correct the docblocks
plus the `/commit` prompt sentence that still described absolute `cd` as
never rewritten.

R1-1, R1-3, R1-4 from bot review round 1 on a0a73649.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvvu8is442

---------

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-10-31 16:04:54 +08:00
11b40836d3 fix(ci): exempt non-TUI packages from the parity impact gate (#15937)
* fix(ci): exempt non-TUI packages from the parity impact gate

* fix(ci): exempt the manifest-less SDK trees from the parity gate

The exclusion list was audited from workspace manifests, so
packages/sdk-java, packages/sdk-python and packages/zed-extension — none of
which carries a package.json — stayed outside it and still selected the
component lane for a PR touching only them.

Add the three trees to the exclusion, pin each with a single-file case in the
classifier contract suite, and record why they are hand-listed in the design
doc pair.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvvf8ed23g

* fix(ci): name the policy exclusion and cover the CUA driver tree

The exclusion branch skipped every matched path before recording a
reason, so a PR excluded on policy reported the same "No TUI impact"
as a PR whose paths the classifier never mapped. Record the reason once
inside that branch (R1-2).

packages/cua-driver is outside the --cli-only closure: node-repl
declares @qwen-code/cua-sdk, but the build filter pulls node-repl
without its dependencies and no TUI suite imports the SDK. Exempt it
with a single-file row and a rename row (R2-1).

The workspace-excluded trees were a third hand-maintained copy of the
set pnpm-workspace.yaml states, with nothing reconciling them. A new
case reads pnpm-workspace.yaml and asserts every excluded tree still
classifies as no-impact (R2-2).

The design docs claimed the excluded packages are never built by these
gates. npm run bundle compiles integrations/external-context into
dist/mem0 through hard-coded esbuild entries, so say what actually
happens and name esbuild entries and copy_bundle_assets copies as
inputs the manifest audit cannot see (R2-3).

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvvnt1g33t

* test(ci): reconcile workspace negations by value, not literal spelling

* test(ci): pin the policy reason for every excluded workspace tree

R4-1: the reconciliation test only asserted outputs.component, the value the
classifier starts with, so narrowing reasons.add(...) for any tree except
cua-driver silently reverted the human-facing diagnostic to "No TUI impact"
while all 163 tests stayed green.

- assert outputs.reason inside the loop, not only for the CUA driver tree
- extract excludedTrees(yamlText) and feed it a fixture with a trailing-comment
  negation and an !integrations/... negation, so the by-value derivation is
  exercised for the spellings its comment claims

Witness (both mutants, unmutated suite 163/163 green):
- reasons.add narrowed to packages/cua-driver/: reason assertion red
- derivation reverted to the old literal regex: fixture assertion red

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvvs3czk3z

---------

Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-10-31 15:49:57 +08:00
易良andQwen-Coder cd4c917fcf fix(ci): route helper-test tmpdir off the host /tmp tmpfs (#15979)
* fix(ci): route helper-test tmpdir off the host /tmp tmpfs

* test(ci): pin the helper-battery tmpdir routing against comments

Two gaps in the #15979 change:

- The pin used toContain/indexOf on the raw run block, so prefixing the
  export with `# ` disabled the routing while the guard stayed green.
  Filter comment lines before asserting, like the lint-history pin already
  does, and pin the private dir + EXIT trap the step now creates.
- The step routed TMPDIR straight into the shared RUNNER_TEMP root. Copy
  the unit lanes' second half too: mktemp -d under RUNNER_TEMP and reap it
  on EXIT, so a cancelled battery does not leave its fixture tree behind
  (its own after() hook does not run on a signal).

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvvyiud548

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-31 15:35:03 +08:00
易良 46d825d115 Revert "fix(ci): isolate web-shell smoke from shared runner networks (#15886)" (#15961)
This reverts commit cc6de38cb5.
2026-10-31 02:36:35 +00:00
易良andQwen-Coder 5f5664e596 test(managed): treat targeted cleanup loads as non-resuming recovery (#15926)
The crash audit classified every post-failure cleanup load as a failed
cancellation takeover because cleanup names a prompt other than the client
prompt. Both cleanup branches return before any resume, so a targeted cleanup
is a non-resuming recovery mode; keep requiring at least one accepted
cancellation takeover.

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-31 00:02:45 +08:00
504556b6a9 ci(e2e): time-box the artifact download retry and the two uncapped legs (#15917)
Each leg downloads the shared build archive twice: once, and again from
the retry that only fires when the first attempt failed. Only the first
attempt carried timeout-minutes, so a retry that stalls held its runner
with no ceiling at all — up to GitHub's 360-minute default. Run
37119220504 (#13284) already showed both attempts lost inside one
window on a macOS shard while its sibling downloaded the same artifact.

Two limbs:

- Give the four 'Download build artifact (retry)' steps
  timeout-minutes: 10. The value is not new: the four first-attempt
  downloads in the same jobs already pin 10.
- Give e2e-test-macos and isolated-nightly a job-level
  timeout-minutes: 60. The two other artifact-consuming legs
  (e2e-test-linux, e2e-interactive-opentui) already use 60, and 60 is
  also the ceiling the shard-retry budget gate in run-e2e-tests.sh
  takes its 25-minute reserve out of. `build` keeps its own 30 — it
  produces the archive rather than consuming it.

web-shell-browser-regression has the same missing job ceiling but is
left alone: it is outside this finding's scope and does not consume the
build artifact.

The retry steps keep their absence of continue-on-error — the first
attempt's continue-on-error is what makes the retry reachable, and
copying it onto the retry would let a stalled download pass silently
into the unpack step. isolated-nightly's job-level continue-on-error is
untouched, so its 60-minute ceiling frees the runner without changing
the leg's conclusion.

Pinned in scripts/tests/e2e-workflow.test.js: both limbs, the retry's
visible failure, and a regression guard over the already-landed values
(build 30, the two consuming legs 60, the four first-attempt steps 10).

Refs #12132


Patrol-Run: qwen-issue-patrol/jmvuv88frpw

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-10-30 23:43:13 +08:00
易良andQwen-Coder 38337f7257 test(managed): count only accepted cancellation takeovers (#15912)
Co-authored-by: Qwen-Coder <[email protected]>
2026-10-30 19:43:26 +08:00
易良andQwen-Coder cc6de38cb5 fix(ci): isolate web-shell smoke from shared runner networks (#15886)
* fix(ci): isolate web-shell smoke from shared runner networks

Co-authored-by: Qwen-Coder <[email protected]>

* docs(ci): clarify hosted smoke font guard behavior

Co-authored-by: Qwen-Coder <[email protected]>

* docs(ci): explain dormant smoke pool steps

Co-authored-by: Qwen-Coder <[email protected]>

* docs(ci): stop overstating which dormant smoke guards are pinned

The retained comment claimed every self-hosted step's guard is pinned by
checkout/preflight tests. Only two of six are: `Recreate runner Git
metadata` (scripts/tests/qwen-runner-checkout-workflow.test.js) and
`Use pre-installed Node.js (self-hosted)`
(scripts/tests/no-ak-integration-ci.test.js). `Check host font baseline
(self-hosted)` is named by no test file, and its
`runner.environment == 'self-hosted'` guard can never be true under
`runs-on: 'ubuntu-latest'` — so it is parked, not pinned.

Say which two are pinned and that the font baseline guard is parked, and
re-measure the ci.yml size ratchet entry (155625 -> 155708).

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvugxtx22a

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-30 19:43:04 +08:00
5cba899e46 docs(ci): correct the qwen-autofix job cap and budget table to match the workflow (#15914)
Two entries in .github/workflows/qwen-autofix.md restated review-address
bounds that the authoritative workflow no longer has:

- af-039 said "The 300-minute job cap puts the worst case at 5
  runner-hours per slot (100 across the fleet at 20)". The cap is
  timeout-minutes: 345 (qwen-autofix.yml:3670), so it becomes 5.75
  runner-hours per slot and 115 across the fleet at the max-parallel
  fallback of 20 (:3679).
- af-050's worst-case budget table listed "Repair 20" and
  "worst case 280 => job timeout 300". The Repair deterministic
  rejection step is bounded at 70 minutes (:5540, "the 60-minute budget
  below plus the same 10-minute margin"), so the row becomes 70 and the
  table's own six rows sum to 330, quoted against the real 345 cap. The
  trailing "on ubuntu-latest, whose own ceiling is 360" clause is left
  as-is: 345 < 360 keeps it coherent, and re-deriving the conditional
  runs-on expression (:3669) is out of scope.

The table total is 330, leaving 15 minutes against the 345 cap. The
workflow comment at :3667-3668 separately describes a 25m reserve for
the four-step unarmed chain (130+60+70+60 = 320), which excludes the
setup and report rows this table itemises. The two are deliberately not
conflated here.

Adds a test that derives every quoted figure from qwen-autofix.yml and
requires the doc to agree, so neither entry can drift silently again.
The existing sum check reads only the workflow and structurally cannot
see a doc that restates the same numbers in prose.

Witness:
- red, doc unmodified: "x keeps the af-039 and af-050 budget prose in
  step with the job cap -> expected '# `qwen-autofix.yml` - design
  record...' to contain '345-minute job cap'"; Tests 9 failed | 327
  passed | 14 skipped (350), rc=1
- green, both sites fixed: that test passes; Tests 8 failed | 328
  passed | 14 skipped (350), rc=1. The 8 are pre-existing
  local-environment noise, identical to the unmodified-base baseline
  (Tests 8 failed | 327 passed | 14 skipped (349), rc=1).


Patrol-Run: qwen-issue-patrol/jmvuqxwvapt

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-10-30 19:42:46 +08:00
7c9e79e70b perf(release): reuse verified native audio prebuilds across releases (#15854)
* perf(release): reuse verified native audio prebuilds across releases

Every release rebuilds the five native audio prebuilds even when nothing
that affects those binaries changed. The build is short; the wait is not.
For v0.26.3 the Linux x64 prebuild queued 74 minutes to execute for 50
seconds, and the collection step queued again after it — and `publish`
depends on that collection, so the whole release sits behind it.

Fingerprint the audio build inputs and reuse an artifact from an earlier
successful official main release when the fingerprint matches. The key
covers the audio package, the dependency lock, the package manager pin and
the contents of the producing workflow and this helper, so the Node pin and
the prebuildify target arguments are part of it. The package version is
excluded deliberately: no part of the native build reads it, and including
it would make reuse impossible between releases.

Reuse means executing bytes an earlier run produced, so provenance is
checked rather than assumed: exact fingerprint name, unexpired, same
repository for both run and head run, and a completed successful
release.yml run on main from workflow_dispatch or schedule. The pinned
download-artifact already verifies the artifact digest.

Retention is checked differently at selection and consumption. Selection
requires 48 hours of headroom so the artifact outlives the release;
consumers require only that it has not expired. Re-applying the margin
against a later clock would reject an artifact the release had already
committed to, after the native matrix was skipped and with no fallback.

Lookup, trial download and payload validation are all non-fatal, and the
reuse ID is published only once a trial download has validated; an empty ID
runs the full matrix, which is today's behaviour. The publisher does not
trust that decision: it recomputes the fingerprint from its own checkout,
re-verifies the source run, and validates the payload again, fatally — by
then the matrix was skipped, so a bad artifact must stop the release.

This does not depend on ECS_RELEASE_HOST_RUNNER_ENABLED and takes effect
with the switch unset.

The collector needed three fixes to be a trustworthy producer. Its
`rm -rf -- "$GITHUB_WORKSPACE/packages/audio-capture/prebuilds"` resolved
through whatever `packages` was on a persistent host: with a leftover
symlink it deleted an external tree and exited 0, and the following
download wrote through the same symlink. It now wipes through a shared
preflight that canonicalises the workspace and refuses paths resolving
outside it. It also ran `node` with no Node contract, inheriting Node from
the hosted image by accident. And its release-only label pinned `x64` while
the runners holding that label are ARM64, which never matches and queues
until timeout.

Contract tests pin the wiring, each confirmed to fail against an injected
regression. Design doc in both languages.

* fix(release): reconcile audio reuse workflow contracts

Share the pre-checkout wipe, preserve the workflow size cap, and let fingerprint failures rebuild native audio.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(ci): use block-style release permissions

Co-authored-by: Qwen-Coder <[email protected]>

* fix(release): drop x64 from publish's release-host expression

The review's unresolved item: collect got the ARM64 label fix but publish kept
["self-hosted", "linux", "x64", "ecs-qwen-release-host"], which can never
match the ARM64 machines holding that label, so the OIDC-carrying job would
queue forever once ECS_RELEASE_HOST_RUNNER_ENABLED is true. Same expression as
the collect fix, same one-word drop.

* docs(release): clarify audio reuse rollout and validation

Co-authored-by: Qwen-Coder <[email protected]>

* test(release): cover artifact overwrite and contained symlink guards

* test(web-shell): include transcript coverage in recovery fixture

Bring the fixture correction from #15883 into this branch to address the observed smoke failure.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(release): make audio cache seeding best effort

Keep the validated same-run artifact usable when retaining a keyed copy fails. Pin the reuse wiring and correct the retention and producer fixtures without changing release trust boundaries.

Co-authored-by: Qwen-Coder <[email protected]>

* chore(test): apply prettier to the audio prebuild fixtures

* docs(release): clarify audio reuse run provenance

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: zzj <[email protected]>
2026-10-30 11:27:38 +00:00
易良andQwen-Coder bf074c4ad8 test(managed-agent): retry the items read through a lagging projection (#15910)
* test(managed-agent): retry the items read through a lagging projection

The public reader answers 503 message_projection_pending while projection
catches up, and #15870 documents that as retryable. The crash fixture asserted
200 on the first response, so a coordinator restart -- exactly when projection
lags -- failed the gate with 'expected: 200' instead of polling (#15909).

Poll the read through that one response and widen the await budget to 60s;
every other status still fails the way it did.

* test(managed-agent): preserve projection retry bounds and wire contract

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-30 18:25:04 +08:00
易良andQwen-Coder 84aee5b9b7 test(web-shell): include transcript coverage in recovery fixture (#15883)
* test(web-shell): include transcript coverage in recovery fixture

Co-authored-by: Qwen-Coder <[email protected]>

* test(web-shell): complete recovery event fixtures

Remove the unevaluated transcript type guard and supply required event terminal flags without changing assertions.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(ci): use runner temp for Web Shell browser smoke (#15884)

* fix(ci): use runner temp for Web Shell browser smoke

Co-authored-by: Qwen-Coder <[email protected]>

* fix(ci): resolve runner temp in the browser step context

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>

* fix(ci): partition the complete macOS test suite into bounded jobs (#15888)

* fix(ci): use runner temp for Web Shell browser smoke

Co-authored-by: Qwen-Coder <[email protected]>

* fix(ci): resolve runner temp in the browser step context

Co-authored-by: Qwen-Coder <[email protected]>

* fix(ci): partition the complete macOS test suite into bounded jobs

Co-authored-by: Qwen-Coder <[email protected]>

* fix(ci): account for the macOS aggregate job budget

Record the additional five-minute aggregate without changing any test execution timeout.

Co-authored-by: Qwen-Coder <[email protected]>

* test(install): isolate Windows fixtures from persistent PATH (#15889)

Co-authored-by: Qwen-Coder <[email protected]>

* fix(ci): write the macOS shard matrix in block style and pin its gates

The seven-row matrix used YAML flow mappings, which `.yamllint.yml`
forbids (`braces: forbid: 'non-empty'`), so the `Run yamllint` step
cannot go green on this branch. Rewrite the rows in the block style
every other workflow matrix in this repo already uses.

Add the test pins the shard plumbing was missing:

- the `cli`/`core` dispatch argv, including the bare `--` that keeps
  `--shard` forwarded to vitest instead of consumed by npm, and the
  per-group `NODE_OPTIONS` choice
- `id: 'install'` on `test_macos_shards`, which the suite's
  `steps.install.outcome` gate silently reads as '' if it is renamed
- the aggregate `test_macos` gate against the shards' gate, so the
  preserved check name cannot report `skipped` while the legs fail

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvu6810z1y

* fix(ci): preserve shell fixture gates on macOS system Bash

Handle empty issue families under nounset without mapfile and explicitly reject either missing OSS credential. Keep failure assertions and include shell diagnostics.

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>

* fix(ci): keep macOS aggregate failures eligible for infra retry

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-30 18:24:19 +08:00
易良andQwen-Coder 54fd8e0372 fix(ci): route the E2E build job through the pool-first runner expression (#15885)
* fix(ci): route the E2E build job through the pool-first runner expression

* fix(ci): carry the full self-hosted contract on the E2E build job

* test(ci): pin the pool contract's step order and sync the build-routing design note

* test(ci): pin the npm-cache position and heal body on every pool job

The pool-contract loop only asserted that `Configure persistent npm cache
(self-hosted)` exists, so moving it below `Install dependencies` or dropping
its `if:` stayed green while the pool install silently fell back to the
default npm cache and re-downloaded through the slow egress the step exists
to avoid. It also left the build job's ownership-heal body unpinned: the full
body was only asserted on the Linux leg, so dropping the `sudo -n` fallbacks
from the build copy stayed green while the next checkout on a pool host still
holding root-owned leftovers dies with EACCES.

Pin the cache step's position and `if:`, and compare every pool job's heal
body against the leg copy the docker-lock ownership suite already pins in
full. Three mutants (cache step moved below install, cache `if:` stripped,
build heal `sudo -n` dropped) now each fail one test; unmutated 130/130 pass.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvu6810z1y

* docs(design): pair the E2E build-once note with its zh-CN version

docs/design/README.md asks that updating an existing single-language design
adds the missing version and aligns the pair, and this PR rewrites the
section that records why the build left the hosted runner. Add
e2e-build-once.zh-CN.md with the same section order and the same decisions,
and link both directions immediately under each title.

Also drop the "only hosted-pinned structural job" claim from the English
note: `e2e-interactive-opentui` is hosted-pinned too, which the same note
already records two paragraphs down. The remaining sentence keeps the actual
cause - every leg waits on `build`, and `build` sat on hosted ubuntu.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvu6810z1y

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-30 18:24:15 +08:00
易良andQwen-Coder d896c1816a test(managed): align recovery and transcript fixture contracts (#15899)
* test(managed): record which load path produced each post-crash entry

HostedProcessCrashIT fails on main with postFailureLoads.every(Boolean), but
the recorded sequence changes shape between runs (#15898: 9 vs 10 entries,
different true positions, one run with none), so the current message cannot
say which of the connector's load paths sent the offending request.

Record a compact origin discriminator per entry — cancellationTakeover,
passiveManagedRuntimeRecovery, driveRuntimeRecovery, mcpManagementOnly and
whether cancellationPromptId was present — and include it in both assertion
messages. The assertion itself is unchanged: 'no tool replay after a crash'
is the property it protects, and the next red run should name its own origin
instead of being guessed at.

* test(managed): name the load path behind each post-crash entry

Both review findings were correct: the origin string added in fad5d81a
still could not name the culprit it exists to name.

Two load shapes reach POST /session/:id/load carrying markers the origin
string never read, so they rendered byte-identical to a bare re-attach:

- a recovery-cleanup load sets only recoveryCleanupPromptId
  (LoadHarnessSession.forRecoveryCleanup throws when passive/drive/
  takeover/lifecycle/promptId are set at all), and
  QwenHostedHarnessConnector.doRecheckRecoveryCleanup issues one;
- a lifecycle load sets only lifecycleAuthority
  (QwenHostedHarnessConnector.lifecycleAttachment builds it with
  passive=drive=takeover=false).

promptId could not rescue them: withCancellationPromptId throws unless
passiveManagedRuntimeRecovery && cancellationTakeover, so promptId=true
implies takeover=true and the marker is always false on exactly the
entries every(Boolean) rejects.

Recording also happened eleven lines before await fetch(target, ...), so
a load the daemon refused was indistinguishable from an admitted one.
This driver already expects that refusal for a plain attach
(assert.equal(loaded.error, 'hosted_turn_recovery_required'), HTTP 409),
and the connector then performs the takeover load that actually replaces
the runtime. Move the recording after the response and add status, so a
refused probe that changed nothing no longer prints as the forbidden
admitted plain re-attach.

Verified by mutation over the eight load shapes that can reach this
endpoint: the previous marker set collapsed four of them (refused bare
reattach, admitted bare reattach, recovery-cleanup, lifecycle) into one
indistinguishable string; each of the three added markers is
load-bearing, since dropping any one reintroduces a collision. The
assertion itself is unchanged.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvugxtx22a

* test(managed): distinguish cleanup loads and response outcomes

Co-authored-by: Qwen-Coder <[email protected]>

* test(managed): align recovery and transcript fixture contracts

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-30 18:17:02 +08:00
易良andQwen-Coder 48a110cea2 test(web-shell): select retained radio without pointer coordinates (#15896)
Co-authored-by: Qwen-Coder <[email protected]>
2026-10-30 14:17:43 +08:00
易良andQwen-Coder 87022fa7c6 test(managed-agent): select managed JDBC in Hosted fault fixtures (#15882)
Co-authored-by: Qwen-Coder <[email protected]>
2026-10-30 13:57:42 +08:00
70be140e14 fix(release): reduce hosted runner queue delays (#15832)
* perf(release): reuse verified native audio prebuilds across releases

Every release rebuilds the five native audio prebuilds even when nothing
that affects those binaries changed. The build is short; the wait is not.
For v0.26.3 the Linux x64 prebuild queued 74 minutes to execute for 50
seconds, and the collection step queued again after it — and `publish`
depends on that collection, so the whole release sits behind it.

Fingerprint the audio build inputs and reuse an artifact from an earlier
successful official main release when the fingerprint matches. The key
covers the audio package, the dependency lock, the package manager pin and
the contents of the producing workflow and this helper, so the Node pin and
the prebuildify target arguments are part of it. The package version is
excluded deliberately: no part of the native build reads it, and including
it would make reuse impossible between releases.

Reuse means executing bytes an earlier run produced, so provenance is
checked rather than assumed: exact fingerprint name, unexpired, same
repository for both run and head run, and a completed successful
release.yml run on main from workflow_dispatch or schedule. The pinned
download-artifact already verifies the artifact digest.

Retention is checked differently at selection and consumption. Selection
requires 48 hours of headroom so the artifact outlives the release;
consumers require only that it has not expired. Re-applying the margin
against a later clock would reject an artifact the release had already
committed to, after the native matrix was skipped and with no fallback.

Lookup, trial download and payload validation are all non-fatal, and the
reuse ID is published only once a trial download has validated; an empty ID
runs the full matrix, which is today's behaviour. The publisher does not
trust that decision: it recomputes the fingerprint from its own checkout,
re-verifies the source run, and validates the payload again, fatally — by
then the matrix was skipped, so a bad artifact must stop the release.

This does not depend on ECS_RELEASE_HOST_RUNNER_ENABLED and takes effect
with the switch unset.

The collector needed three fixes to be a trustworthy producer. Its
`rm -rf -- "$GITHUB_WORKSPACE/packages/audio-capture/prebuilds"` resolved
through whatever `packages` was on a persistent host: with a leftover
symlink it deleted an external tree and exited 0, and the following
download wrote through the same symlink. It now wipes through a shared
preflight that canonicalises the workspace and refuses paths resolving
outside it. It also ran `node` with no Node contract, inheriting Node from
the hosted image by accident. And its release-only label pinned `x64` while
the runners holding that label are ARM64, which never matches and queues
until timeout.

Contract tests pin the wiring, each confirmed to fail against an injected
regression. Design doc in both languages.

* fix(release): move the release build and SDK build off the hosted queue

Behind the existing ECS_RELEASE_HOST_RUNNER_ENABLED switch, prepare the
version commit and produce the CLI build outputs on an isolated release
runner, and split the TypeScript SDK into an isolated producer plus the
existing hosted publisher. Eligible Linux downstream jobs join the same
pool. With the switch unset every path is exactly today's behaviour.

The final npm publication stays on GitHub-hosted runners. npm trusted
publishing and provenance require a cloud-hosted runner, so moving the
publisher itself would break the release rather than speed it up — only
the build work ahead of it can leave the queue.

Each migrated job runs one shared preflight before checkout, referenced
as a '$/' self-repository action so it resolves at the running commit
without a prior checkout. Release-only labels carry no architecture
component: the runners holding that label are ARM64, and an "x64"
component never matches instead of falling back to hosted. Jobs that
genuinely need amd64 — the OSS mirrors and the managed runtime, which
execute a hardcoded linux-amd64 ossutil — keep their hosted path, tracked
in #15847. The VSCode build routes by matrix.universal so only the
dependency-free leg migrates.

The SDK publisher consumes the exact tested tarball through a same-run
artifact and refuses to overwrite different already-published bytes on
retry. Audio prebuild reuse is the base of this branch and is specified
separately; the isolated build job is a second consumer of that artifact
and is held to the same re-verification contract as the publisher.

Rebased onto the audio prebuild reuse branch, so that mechanism and its
contract tests are no longer duplicated here.

* fix(release): reconcile audio reuse workflow contracts

Share the pre-checkout wipe, preserve the workflow size cap, and let fingerprint failures rebuild native audio.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(ci): use block-style release permissions

Co-authored-by: Qwen-Coder <[email protected]>

* fix(release): drop x64 from publish's release-host expression

The review's unresolved item: collect got the ARM64 label fix but publish kept
["self-hosted", "linux", "x64", "ecs-qwen-release-host"], which can never
match the ARM64 machines holding that label, so the OIDC-carrying job would
queue forever once ECS_RELEASE_HOST_RUNNER_ENABLED is true. Same expression as
the collect fix, same one-word drop.

* docs(release): clarify audio reuse rollout and validation

Co-authored-by: Qwen-Coder <[email protected]>

* docs(release): reserve runner slots on existing ECS hosts

* fix(release): quote audio workflow scalar values

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: zzj <[email protected]>
2026-10-30 01:08:54 +08:00