Commit Graph
965 Commits
Author SHA1 Message Date
jinyeandQwen-Coder 7ad260bceb feat(managed-agent): persist Session execution diagnostics (#16642)
* feat(managed-agent): add Session resource and agent-trace diagnostics

Expose configured Skills/MCP resources with bounded persisted discovery
history and the direct child-agent invocation trace through Public GET
and WebShell query routes, and add the Session diagnostics dialog with
Resources and Child agents tabs.

Reads use existing persisted records only: no write path, storage
version change or DDL. Split from #16475.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(managed-agent): persist Session model statistics and request context

Collect Session-owned model statistics across main, Hook and compression
sources and the last committed main-request context, fold them at the
native commit boundary, project them into the existing Session records,
and expose them through Public GET and WebShell query routes with the
Context and Model statistics tabs.

The diagnostic resource raises the stored reader floor to storage 3; the
diagnostics-aware csi-files-retirement/2 profile carries it without
widening CSI execution admission. Split from #16475.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(managed-agent): persist Session execution diagnostics

Record per-invocation tool admission, execution and delivery, explicit
built-in file operations, loaded Skill instructions and expandable child
invocations alongside the existing message, intent and receipt commits,
project them atomically into the Session records, and expose activity
statistics and pages through Public GET and WebShell query routes with
the Activity tab and invocation tree.

Execution evidence raises the stored reader floor to storage 4. Split
from #16475 and carries the content previously reviewed as #16499.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* refactor(cli): consolidate hosted tool-turn diagnostic bookkeeping

Route policy refusals through one denied() helper, merge invocation
overrides through noteDiagnostic(), reset per-batch diagnostic state in
one place, share the Runtime Broker bypass predicate with the tool-kind
classifier, and validate diagnostic file paths with the same rule the
evidence parser enforces.

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-09 07:13:59 +00:00
60e588eb63 test(ci): isolate the checkout hook fixture from the release runner git config (#16684)
* test(ci): isolate the checkout hook fixture from the release runner git config (#16680)

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

* test(ci): witness the release gitconfig pin on every lane (#16680)

The fixture only overrode GIT_CONFIG_GLOBAL under the release runner's
ambient, so deleting the override stayed green on ordinary lanes. Plant the
same empty ambient file everywhere; the positive control now fails when the
pin is removed. Commit identity moves to -c so that failure lands on the
hook assertion rather than git's missing user config.

* test(ci): witness the release gitconfig premise in the hook fixture (#16680)

The release ambient planted on every lane was shadowed by the fixture's
GIT_CONFIG_GLOBAL pin and observed by no assertion, so deleting it stayed
green and silently retired the witness this fixture exists to add. Pin the
premise with a release-ambient checkout and assert the planted hook does not
fire; removing the ambient's GIT_CONFIG_GLOBAL now reds that assertion.

* test(ci): observe the release ambient in the hook fixture's bare env (#16680)

The helper pinned the planted global config into every git call, so the release ambient spread was shadowed and the negative control re-supplied it at the call site; deleting the spread stayed green. Keep the base env ambient-only, pin the positive control explicitly, and let the negative control run on the bare base env so removing the ambient reds it.

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

* test(ci): keep the planted gitconfig as the checkout fallback (#16680)

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

---------

Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-09 14:43:24 +08:00
829ef49acf fix(ci): add bounded, idempotent retry to the SDK release branch push (#16705)
* fix(ci): retry sdk release branch push with bounded backoff

The release-sdk workflow pushed the SDK release branch with a single
bare git push. GitHub fails closed when it cannot decide in time
whether the push may create or update workflow files, which aborted
the stable release even when the push had actually landed.

Retry the push up to 3 times with 30s/60s backoff, reconciling with
the remote after each failure: an already-landed push is treated as
success, a diverged remote branch is a hard error, and the branch is
never force-pushed.

Fixes #16490

* fix(ci): reconcile release branch via ls-remote output, not fetch

A bare git fetch in the reconcile path ran unguarded under the
runner's bash -eo pipefail: a transient fetch failure after a
false-negative push would abort the step before the remaining
retries, reproducing the original failure shape. Read the remote tip
from a single ls-remote instead, with || true so a failed lookup
falls back to the plain retry path.

* fix(ci): match release branch by full ref in push reconciliation

git ls-remote --heads matches its pattern against ref tails on slash
boundaries, so a suffixed branch (e.g. backup/release/...) would add
a second line and make a landed push look diverged. Query the full
refs/heads/ ref so exactly one line can come back.

* fix(ci): harden SDK release push retry per round-1 review

- run persist_source under set -euo pipefail so the ls-remote || true guard
  is load-bearing and its rationale accurate
- correct the reconcile comment: the observed #16490 shape left nothing
  remotely, so the bounded re-push is the remedy and the reconcile guards a
  possible partial --follow-tags push
- annotate each absorbed push failure with a ::warning:: naming the attempt
- pin all four loop branches behaviorally in release-sdk-workflow.test.js
  with a stubbed git/sleep harness; flipping the reconcile comparison,
  deleting the divergence abort, or removing the warning turns a case red

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

---------

Co-authored-by: qwen-code-dev <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-09 13:42:23 +08:00
0e7bf516d1 feat(managed-agent): persist Session model statistics and request context (#16641)
* feat(managed-agent): add Session resource and agent-trace diagnostics

Expose configured Skills/MCP resources with bounded persisted discovery
history and the direct child-agent invocation trace through Public GET
and WebShell query routes, and add the Session diagnostics dialog with
Resources and Child agents tabs.

Reads use existing persisted records only: no write path, storage
version change or DDL. Split from #16475.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(managed-agent): persist Session model statistics and request context

Collect Session-owned model statistics across main, Hook and compression
sources and the last committed main-request context, fold them at the
native commit boundary, project them into the existing Session records,
and expose them through Public GET and WebShell query routes with the
Context and Model statistics tabs.

The diagnostic resource raises the stored reader floor to storage 3; the
diagnostics-aware csi-files-retirement/2 profile carries it without
widening CSI execution admission. Split from #16475.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(managed-agent): align model context validation

Reject zero contextWindowSize values consistently in Java and OpenAPI, pin the shared zero-window and duplicate-gap regressions, and localize the diagnostics Context tab.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-11-09 03:32:10 +00:00
2aac39a17b ci: route the web-shell visuals capture for fork PRs to the disposable pool (#16617)
* ci: route the Test aggregate gate and web-shell visuals capture to the disposable pool

Both jobs match the ecs-qwen-ephemeral pool's security model: the Test
aggregate runs one shell line and checks out nothing, and the visuals
capture builds untrusted PR code with a read-only token and no
long-lived secrets. Routing them off the hosted queue removes the
9-minute waits the one-line gate saw after its shards finished, and
takes a 45-minute render job off hosted capacity.

The MAINTAINER_ECS_RUNNER_DISABLED kill-switch reverts both to hosted.
The E2E OpenTUI leg deliberately stays hosted: it carries long-lived
model secrets, which the disposable pool's boundary forbids until a
separate trusted pool exists.

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

* ci: complete the pool split for the visuals capture and sync the design docs

Review follow-up on #16617. The capture kept the unconditional
setup-node download, which can fail outright on the pool where
nodejs.org may be unreachable: gate it to hosted, add the self-hosted
Node preflight and the disk-floor gate (the tui-parity shape), and pin
all of it in the routing battery.

The slim image also lacks the DejaVu font baseline and ffmpeg; one
best-effort self-hosted apt step installs both so a blocked Debian
archive degrades to fallback fonts and .webm-only recordings instead
of sinking the preview.

Record the migration in both design-doc languages (superseding the
browser-dependency exclusion for this job), refresh the deployed slot
count, and document that the aggregate gate intentionally no longer
follows the dispatch fleet inputs. Extend the secret scan to the
bracket-indexed access form and drop the no-op sameRepo loop.

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

* test(ci): pin the ffmpeg install line and the degradation branch

Both assertions matched text that survives the change they exist to catch:
/ffmpeg/ also matches the degradation warning's own wording, so dropping
the package from the apt list stayed green, and /::warning:: only proved
the string appears somewhere in the step.

- Match the install statement itself: `apt-get install … ffmpeg` on one
  line, so only the package list satisfies it.
- Require the warning to sit in the guard's failure branch (`else` today,
  `||` accepted). That branch is what makes the step degrade instead of
  sinking the job — the step carries no continue-on-error.

Verified by mutation on web-shell-visuals.yml: dropping ffmpeg from the
apt list, deleting the else branch, and removing the guard entirely each
turn this suite red (110 -> 109 passed) at the new assertion lines; every
revert restores the workflow byte-for-byte.

* fix(ci): drop fonts-dejavu-mono from the pool apt step and qualify the degradation claim

The pool image is node:22-bookworm-slim (Debian 12, now oldstable), where
fonts-dejavu-mono has no build — the mono face already ships inside
fonts-dejavu-core. One unknown package fails the whole apt transaction
before anything installs, so the step would have taken its warning branch
on every pooled capture and installed neither the fonts nor ffmpeg.

The workflow comment and both design docs also overclaimed the degradation
contract: the earlier `playwright install --with-deps` step installs apt
packages without continue-on-error, so a blocked Debian archive sinks the
preview rather than degrading it. The best-effort wording now covers only
the extra font/ffmpeg install this step performs.

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

* fix(ci): route the required gate with the shards and harden the pooled capture

The required Test aggregate queued 110 minutes on the manually sized
disposable pool with an empty runner (job 128729488158) while sibling
jobs were served in ~3; it now follows the classifier's ubuntu_runner
like the sibling aggregates, so the kill-switch also works on fork runs.

The first pooled capture runs measured every remaining review finding:
pnpm ENOSPC after the shared 2 GiB floor passed an 89%-used slot (run
42478651795), a runner lost to CPU/memory starvation from unpinned
Playwright workers (run 42494910412), and a 45-minute cap cancelled
before any render with installs alone at 22m18s. The capture now carries
a lane-calibrated floor (8 GiB / 85%), --workers=2 on both render passes,
and a routed 90/45 timeout. The apt step gets bounded calls and a
post-install font check; its comment no longer claims the archive itself
degrades.

Pins: the secrets guard walks parsed expressions (toJSON(secrets) and
bracket forms now fail) and bounds job-level permissions; the pool label
derives from the pool config; the whole apt package list, the disk-floor
overrides, the routed timeout and both worker pins are asserted. Six
single-point mutations (mono re-add, workers drop, bare 45, floor drop,
toJSON(secrets), label rename) all red the suite. Size baseline updated
for the reviewed capture growth.

Suites: routing 110/110, platform-lanes + workflow-size +
main-ci-failure-issue 498 passed / 1 pre-existing skip, Prettier clean.

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

* ci: record the visuals capture's routed timeout in the CI budget

The budget gate counts expression timeouts as unbounded by design; the
routed 90/45 ceiling moves web-shell-visuals.yml from a fixed 45 to one
expression-timeout job, exactly the entry the gate's error names.

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

* fix(ci): keep the pooled visuals capture fork-only

The capture's runs-on was rewritten in acbbc6d9 to read only
github.repository and the kill-switch, so every same-repo PR was routed
onto the disposable pool too — contradicting the design doc's "trusted
and same-repository PR routes stay unchanged" rule and the PR's own
purpose (moving fork CI off GitHub-hosted runners, not relocating jobs
that already had a lane).

The expression now carries the fork clause: untrusted fork PRs ride the
ephemeral pool, same-repo PRs stay hosted. The routed timeout follows the
same fork term (90 pool / 45 hosted), the routing matrix pins both arms
per association, and both design docs record the fork-only scope.

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

* docs(ci): align the capture routing claims with the fork-only reality

Review at f90e7f09 found three self-contradicting rollback claims and a
stale PR description:

- the workflow header said "hosted only under the kill-switch" while
  same-repo PRs are hosted unconditionally;
- the runs-on comment said the kill-switch reverts forks, which the same
  PR's ci.yml comment and the design doc both contradict (fork runs
  receive empty repository variables, so the switch cannot reach them);
- the design docs said same-repo runs "revert to hosted" under the
  switch, but same-repo runs never leave hosted.

All three now read the same verifiable claim: the capture uses the pool
for fork PRs only, same-repo PRs are always hosted, and the fork-path
escape is changing the selector on main. The disk-floor doc text now
matches the code's <85% comparison, and the workflow size baseline is
updated to the current byte count. PR title and body were rewritten via
the API to describe the fork-only capture and the reverted aggregate
gate.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-11-09 10:25:18 +08:00
Shaojin Wen 9da77be628 test(ci): keep the visuals publisher and the failure reporter off the pool (#16561)
* fix(ci): route the visuals publish tail to the persistent pool

The web-shell visuals publisher is a workflow_run tail that touches only
trusted base-repo scripts, yet it sat on ubuntu-latest and queued behind
untrusted fork CI while the ECS pool idled. Route it like the other
trusted lanes: on by default, MAINTAINER_ECS_RUNNER_DISABLED forces
hosted, and a foreign repository falls back to hosted.

The pool reuses workspaces, so the job repairs slot ownership before its
trusted checkout, matching the other routed lanes.

The companion meta-test pins the routing matrix and, in the same file, the
deliberate exception next door: the main-CI failure reporter that files
the issue must stay on hosted runners, because a report queued behind the
pool that caused the failure is the one thing that channel must never do.

* fix(ci): drop reused-slot Git metadata before the publisher's checkout

A reused pool workspace keeps the previous job's .git, hooks included, and
git still runs post-checkout hooks after actions/checkout resets the tree.
Pinning the trusted ref keeps PR code out of the tree, not out of the
process: an old job's hook runs inside this job, which then uses the write
token and the OSS credentials. Remove the metadata before checking out,
matching the other pool-routed checkouts, and add this publisher to the
derived checkout-guard suite so the step cannot be dropped silently.

The routing test now pins the three-step order: ownership repair, metadata
reset, then the trusted checkout.

* test(ci): pin the visuals publisher and the failure reporter off the pool

Review of the routing change found the publisher is not a pool candidate
after all: it carries CI_BOT_PAT and the Aliyun OSS key pair, and a reused
slot exposes two exec surfaces a workspace scrub cannot close — a hook in
the previous job's .git, and one in the runner user's global git config
(core.hooksPath / init.templateDir), both firing after the trusted checkout
and then running in this job with those credentials. Every other job in this
tree carrying that key pair is on a hosted runner.

Withdraw the routing change and keep the guard as a held position: the
publisher and the whole main-CI failure report chain are pinned to hosted
runners as literals, so a future pool expression fails this suite instead of
quietly moving a credential-bearing job. The chain is pinned end to end
because a pool-hosted analyze yields no body and neither writer runs.

* test(ci): derive the pinned job set from the workflow files

The guard listed job ids, so a job added to either pinned workflow later was
never read: adding a `post_summary` that carries the standard pool expression
left the suite green. Iterate the parsed jobs instead — the same values are
asserted today, but a new job is read by the same loop.

Scope stays these two files, because qwen-triage.yml routes its own jobs to
the pool on purpose, and the comparison stays a literal: a pool expression
always carries ubuntu-latest as its fallback.

* docs(ci): describe qwen-triage's pool shapes as they are

The scope comment said triage / publish-triage / verify all reach the pool
through a ternary with a hosted fallback. Two of the six are not that shape:
tmux-testing and verify are bare `['self-hosted','linux','x64','ecs-qwen']`
lists with no fallback, and authorize / publish-tmux were not named at all.

Name all six and both shapes, and scope the fallback reasoning to the two
pinned files as a statement about the form they would grow, not about the
tree.
2026-11-09 09:47:55 +08:00
Shaojin WenandQwen-Coder 0dd106d42f feat(ci): give the autofix address agent failed-check evidence (#16659)
* feat(ci): give the autofix address agent failed-check evidence

The review loop selects a PR when its checks are red, but the agent that fixes it receives check names only and holds no GitHub credentials, so a shard name or a Test check that failed on a lint step costs a round of guessing. The CI Failure Patrol already distils job logs for its classifier; this wires the same redaction and line selection into the address feedback.

A collector prints the failed step names plus a bounded, redacted log excerpt per failing job (first three jobs, 60 lines each, 8 MiB buffer). It runs in prepare before the PR branch is checked out, so the collector and the patrol helper it imports execute from the trusted base. Selection mirrors the workflow's failed-check gates; every failure degrades to less evidence, never a failed round.

Adds behavioral tests, workflow wiring pins with mutation witnesses, and a bilingual design doc.

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

* fix(ci): render the autofix check evidence against a current gh

Review found the log fetch dead on a current gh: it refuses the jobs/<id>/logs endpoint without --allow-escape-sequences (exit 1, empty stdout), and the helper swallowed that into null, so a real round got Failed steps and no excerpt. Apply the three-part shape from autofix-pr/ci-log.ts: pass the flag, fall back to the bare call only on an unknown-flag stderr, and stripVTControlCharacters so raw ANSI never reaches feedback.md or step stdout. The fake gh now reproduces the refusal, so a missing flag turns the suite red.

Also address the non-blocking notes: keep skillLog's whole ~120-line selection so the failure-summary tail survives a noisy log; size the excerpt fence past any backtick run; make the selector comment precise about Still-red vs Failed checks (CANCELLED); and apply the same fetch fix to the patrol's GhClient.jobLog, which was equally log-blind. Tighten the tests the verify report flagged as unpinned (usage guard per argument, 80-char name cap, realistic StatusContext).

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-09 08:43:18 +08:00
Shaojin Wen c73a98951f fix(ci): quarantine a qwen-home neither rm nor sudo can reclaim (#16627)
* fix(ci): quarantine a qwen-home neither rm nor sudo can reclaim

The agent-home reclaim added for #16616 ends in a hard failure whenever a
foreign-owned qwen-home survives both the unlink and the sudo escalation —
which is every pool member without passwordless sudo, so the review is lost
even though the job still has the one permission the repair needs: write
access to RUNNER_TEMP.

Rename the leftover to a timestamped sibling first. A same-parent rename
needs write access to the parent, not to the directory, so it succeeds
where rm and chown cannot, the review proceeds on a freshly created home,
and the previous contents are preserved under a name the warning prints.
A read-only parent refuses the rename, which keeps #16618's fail-fast —
message and probe untouched — as the last rung, and the step header now
says the rename is what saves a survivor rather than promising that every
survivor fails the job.

Rung order is pinned by an executed case: a home the sudo path can delete
must be deleted, never quarantined, or every sudo-capable runner would
strand the previous run's home.

* test(ci): gate the quarantine case to the pool's platform and pin the retry

The quarantine case asserted a Linux-only kernel contract as universal:
renaming a mode-0555 directory needs only the parent's write bit on Linux,
while BSD-derived kernels also rewrite the moved directory's `..` and
answer EACCES, so the case was red 3/3 on APFS. Gate it to the platform the
review pool actually runs on, and state that assumption in the step comment
instead of presenting it as portable.

Also pin the `sudo -n chown -R … || rm -rf` retry that shipped with #16618.
Deleting it left every case green, because the fixture could only express
"sudo deletes" or "sudo does nothing". A stub whose rm leg fails and whose
chown leg hands the directory back exercises the retry, and deleting the
line now reddens exactly that case.

* fix(ci): keep a quarantined home inside every reclaim path

Three review findings on the quarantine rung, all reproduced before fixing.

The renamed leftover escaped the only automatic reclaim this state has:
triage's root container hands ownership back by walking a fixed list whose
review entry is the literal $QWEN_HOME, so a poisoned home used to come
back to the runner user and the next review's plain rm removed it. After
the rename it sat at a name nothing matched, permanently, with one warning
as the only trace. Add the quarantine glob to that hand-back list, and
sweep those names here once the hand-back has made them removable — age
gated like the capture-tools sweep, best-effort, and not matching
$QWEN_HOME itself.

Nothing pinned that the success warning stays silent when the rename is
refused: an ungated echo would tell an operator a leftover was preserved at
a path that was never created, immediately above the fail-fast. The
read-only-parent case now asserts its absence.

The sudo stub answered every subcommand with a delete, so deleting the
primary `sudo -n rm` leg kept the rung-order case green. Key the stub on
the subcommand; that mutant now reddens the case.

* style(ci): trim the sweep comment under the workflow size ratchet

The added prose pushed the workflow 141 bytes past its recorded baseline
plus the 4096-byte allowance, which fails the size lane rather than any
test. Same content, five lines instead of nine; the gate now passes with
151 bytes to spare.
2026-11-09 00:06:22 +00:00
1888aaf49d fix(web-shell): close managed host integration gaps left by the host mount entry (#16200)
* fix(web-shell): close managed host integration gaps from #15928

Follow-up to #15928. Each change below was reproduced against the merged
head before fixing, and every new assertion was checked to fail without
its fix.

The host entry required a Java base URL even when a provider was injected,
so the documented mount only compiled inside test files and a JavaScript
host that dropped the provider silently addressed /undefined/api/... .
The base URL is now optional and only the Java path constructs a client;
with neither input the entry renders the unavailable notice.

Returning to an unconfirmed request's owner was the only navigation that
bypassed the shared open path: it dropped the owning workspace selection
and carried the previous conversation's error into the newly opened one.
Pending records now remember their workspace and the redirect uses the
shared path, so the owner keeps its workspace even after the catalog stops
listing it. An unconfirmed create still returns to the new-conversation
page.

The archived-status callback was forwarded by identity into an effect
that emits a fresh object, so an inline host callback re-rendered without
bound. It is routed through a ref like the other host callbacks.

Host navigation slots sit outside the shell container, where the compiled
stylesheet matches nothing. The entry now wraps each portaled list in the
portal scope and syncs theme class, language and custom properties.

The managed empty state centered every in-flow child, floating the
session header mid-pane and pushing content out of scroll reach once it
grew taller than the pane; it also stayed set while a transcript was
loading or a first send was in flight. Centering moved to auto margins on
the content wrapper and the marker now requires an idle, non-loading
conversation. The welcome header no longer stacks with a failure banner.

Host mode renders no session list, so both focus fallbacks became
no-ops and dropped focus to the document body, and the pane measurement
never re-ran after the waiting state. Focus falls back to the page
wrapper and measurement re-runs when the host supplies a Session ID.

Shared style release was not idempotent: disposing one mount twice
stripped the stylesheet a sibling still used. Both release paths are now
guarded.

The managed-host build mode ran in no gate and wrote inside the directory
the app build empties. It is part of the package build and writes to
dist-managed-host, staying out of the published exports and files.

Weak assertions from the original change were strengthened: the injected
provider test now spies on Java construction, the host-scope teardown
keeps the Session ID constant, the shadow-style test exercises the
constructable-sheet path, and the pending-row exclusion names the real
copy. New coverage pins the host pending key isolation, the notice reset
on a host switch, and the empty failed conversation.

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

* fix(web-shell): render the unavailable notice and gate the pending workspace fallback

Address the two blocking review items on the #15928 managed-host follow-up:

- The mount docs/README promise an unavailable notice when neither a provider
  nor a baseUrl is supplied, but the shell rendered an empty wrapper and the
  test only asserted the Java provider was not called. Render the notice via
  the existing managed.unavailable i18n key and strengthen the test to assert
  the notice text (red without the render, green with it).
- The 'open the original conversation' handler fell back to the on-screen
  summary's workspace, which in the cross-conversation case is a foreign
  workspace. Gate that fallback on pending.sessionId === sessionId so the
  owner is never bound to the wrong workspace when its own workspace is
  unknown.

The third review item (managed-host bundle in npm run build) is a maintainer
call on build cost and is left unchanged pending that decision.

ManagedAgentWebShell 18/18, ManagedSessionsPage 173/173; eslint exit 0,
prettier clean.

* fix(web-shell): theme only our own host slot wrappers and cover centering in a browser

Two review items on this PR remained after a61bfc04.

The slot sync identified its wrapper as the target's first element child.
That is wrong whenever the host slot already holds host chrome, and it is
also wrong when both slots name the same element. A probe against the
reviewed head showed the real consequence is worse than "fails silently":
it wrote lang, the dark class and every custom property onto the host's own
node (host chrome lang fr -> zh-CN) while leaving the actual list wrapper
unstyled. The wrappers now register themselves through a ref, so the sync
only ever touches nodes this entry rendered, and disconnected wrappers are
dropped. Both cases are pinned by tests that fail on the first-child lookup.

The centering claim had no layout evidence: jsdom performs no layout, so the
component test can only pin data-managed-empty, and the visuals bot reported
that no scenario renders this surface. Added a browser regression with its
own host-slot fixture that measures the pane at a cramped viewport. With the
previous justify-content rule the session header sits at -51px, above the
scroll origin and permanently unreachable; with auto margins it sits at 0
and stays in the viewport. The test fails on the old rule and passes on the
new one.

The build-wiring cost stays unchanged pending the maintainer decision. For
that decision, measured locally on this machine: the managed-host pass takes
about 13-14s wall clock against about 9s for the default lib pass and about
3s for the transcript pass, and emits 38 MB.

ManagedAgentWebShell 20/20, the five affected suites 212/212, the new browser
regression 1/1; eslint exit 0, prettier clean.

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

* fix(web-shell): keep the host nav slot from blocking session-list scroll

R1-4: the portal-scope/theme wrapper added around the host navigation slot
generates a block box, so in a bounded flex-column host sidebar it becomes the
flex item and its default min-height:auto pins the nav to content height — the
nav's overflow-y-auto never engages and the list is clipped.

Make the wrapper display:contents: it stays in the DOM (portal-scope attributes
and synced theme variables still apply and inherit) but generates no box, so the
nav remains the host's flex item and scrolls within the bounded slot.

Witness: browser A/B on this head — bounded 300px flex slot, 40 sessions:
navClientHeight 300 / navScrollHeight 1516 (scrollable), last row reachable;
the slot test asserts display:contents (red without it). Theme-sync suite 20/20.

* ci(web-shell): gate the managed host bundle in its own lane, not every build

Resolves the maintainer-call item on this PR. The managed-host mode
externalizes nothing, so riding `npm run build` charged every local build and
every fresh-clone `prepare` with a full bundle of React, xterm, mermaid,
echarts, shiki and codemirror — measured locally at about 13-14s and 38 MB —
for an artifact that stays out of `exports`/`files` and has no in-repo
consumer. The pass bought rot detection and nothing else.

Rot detection now lives in a dedicated workflow that runs the mode on pull
requests and post-merge pushes touching the entry, its build config or the
lockfile. It installs with QWEN_SKIP_PREPARE because the entry needs no
sibling workspace dist/, so the lane costs one bundling pass rather than a
monorepo rebuild first. It runs on an ephemeral hosted runner with a
read-only token and no secrets, and produces no artifact.

A green exit code alone would not catch the rot, so the lane asserts a
non-empty entry containing the mount symbol and the scoped empty-state CSS,
and checks the output stays outside the directory the app build empties and
out of the published exports/files. The local entry point is now
`npm run build:managed-host`.

A workflow meta-test pins both halves of the split: putting the mode back
into the default build fails it, and so does dropping the build config from
the lane's path filter. Both were confirmed by mutation. The .ci-budget
entry is added for the new PR-stage workflow.

Verified locally: actionlint and yamllint clean, CI budget holds, the new
meta-test 9/9, package-scripts and ci-platform-lanes 134 passed, the
all-workflow expression-length assertion passes with the new file, the five
affected web-shell suites 212/212, and `npm run build:managed-host` succeeds
with the lane's verify assertions holding.

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

* ci(web-shell): build core before the managed host bundle

The managed host entry imports one compiled core module, and the package
`exports` map resolves it through `dist/src/`, which `QWEN_SKIP_PREPARE=1`
never produces. The lane failed to resolve it before Rollup had anything
to bundle.

Build that single package instead of running the full `prepare`, and record
the new workflow in the size baseline so the ratchet tracks it.

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

* ci(web-shell): build the SDK dist the managed host entry imports

The managed-host lane installs with QWEN_SKIP_PREPARE=1, so no workspace
dist exists except the ones the lane builds explicitly. managed-host mode
externalizes nothing (`vite.lib.config.ts` returns false), and the entry's
module graph reaches a runtime value import of `@qwen-code/sdk/daemon`,
whose exports map points into `packages/sdk-typescript/dist/`. Building
only core therefore left the bundle step red on this head:

  [vite]: Rollup failed to resolve import "@qwen-code/sdk/daemon" from
  packages/web-shell/client/components/messages/tools/ToolDetailsContext.tsx

Build `@qwen-code/sdk` after core (its `tsconfig.reference.json` only
references `../core`, so the existing step already satisfies it) and pin
the new step in the lane's workflow test.

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

* ci(web-shell): build acp-bridge before the SDK in the managed host lane

The lane installs with QWEN_SKIP_PREPARE=1 and built only core, then the
SDK. The SDK's daemon sources import `@qwen-code/acp-bridge/<subpath>`,
which resolves through that package's `dist/` exports map, and
`tsconfig.reference.json` references core alone, so `tsc --build` never
produces acp-bridge: the lane died on 94 TS2307/TS2339 errors before it
bundled anything.

Build it between core and the SDK, the order live-host-release.yml already
uses. Reproduced both arms locally: without acp-bridge/dist the SDK build
fails with the same 94 errors as run 41732515178; with it, the build exits 0.

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

* fix(web-shell): close the managed host R1 criticals

R1-2: the identity-switch branch dropped an explicit host sessionId when
the provider arrived after mount, because `selection.storageKey` started as
undefined. Guard on a previous identity existing; a real storageKey change
still drops the carried id.

R1-1: a wrapper that mounted in a commit the sync effect did not watch kept
no theme at all. The ref callback now themes the element it just saw, and
the effect discovers our own wrappers by their portal-scope marker instead
of a registry that dropped a detached wrapper for good. `style` joins the
effect deps so a host token change re-copies without a theme flip.

R1-3: the page-wrapper focus fallback parked focus where the discard latch
did not recognise it. The latch now accepts the node the discard parked on
instead of the page wrapper wholesale.

Tests: late-mount + style-only token arms and the late-provider sessionId
arm in ManagedAgentWebShell.test.tsx (22/22 green).

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

* fix(web-shell): keep the managed empty marker after a failed pending restore

A failed pending restore leaves `busy` set until the user discards, so the
new `!busy` term dropped `data-managed-empty` in a state the base marker
still covered, losing the centring and overflow rule this PR adds. The shell
root ref is now a stable callback so a wrapper mounting in a commit the sync
effect does not watch is still themed.

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

* fix(web-shell): drop a carried session id across a provider-less identity switch

Closes the R4 Critical and three review items that made this PR's own
guarantees unverifiable.

R4-1: an identity switch routed through a provider-less render handed the
previous tenant's session to the next one. The provider-less render wrote
`storageKey: undefined`, so the next real provider compared against
undefined and kept the carried id: A -> none -> C mounted C's page on A's
`s1`. The direct A -> C transition dropped it, so this was the one path
around the drop-on-scope-change rule the design doc requires. The selection
now retains the last real identity while no provider is mounted and
compares against it. Both the branch condition and the drop predicate are
gated on a provider being present — the review's suggested shape gated only
the predicate, which I confirmed loops with "Too many re-renders" because
the retained key keeps the condition permanently true.

R2-3: the lane's scoped-CSS assertion could not fail. `data-managed-empty`
is a JSX prop name that survives minification as an object key, so a
stopped injection guard still passed it. It now keys on the injection
preamble (`__qwenWebShellCss`, the per-entry `managed-host` guard) and the
portal scope selector. Witness: renaming the entry in the plugin's guard
produced a build that exits 0 with zero styles — the old grep passed, the
new step fails.

R3-3: the meta-test's no-secrets guard only matched dot access, so
`secrets['TOKEN']` kept it green. It now covers bracket form too, still
without matching the prose in the security comment.

R3-1: the test named for building every workspace dist pinned only the SDK
step. Core and acp-bridge are equally load-bearing, so all three are pinned
along with their required order. Deleting the core step now reds it.

R1-9: the README example imported the pre-move `./managed-host/index.js`,
which the current flat output no longer produces.

The stale comment claiming the entry needs no sibling workspace dist is
corrected: the lane builds core, acp-bridge and the SDK precisely because it
resolves through them.

Verified: the five affected web-shell suites 216/216, the workflow
meta-test 10/10, actionlint and yamllint clean, CI budget holds, eslint
exit 0, prettier clean, and the lane's verify step run verbatim against a
freshly built bundle.

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

* fix(ci): make the managed-host bundle assertions able to fail

R2-3: the portal wrapper ships in its own chunk, and every occurrence of
`data-web-shell-portal-root` in the entry sits inside the injected
stylesheet, so the entry grep passed on a bundle whose wrapper was deleted.
Search the whole output for the quoted JSX prop instead, which the scoped
CSS cannot produce.

R5-1: a job-level `permissions:` block replaces the workflow default, so pin
the job's effective scope rather than the workflow-level block.

R5-2: match any expression that reads `secrets`; `${{ toJSON(secrets) }}` is
as much a read as `secrets.TOKEN`. The workflow's own `${{ github.* }}`
expressions are the control against over-broadening the guard.

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

* fix(ci): trigger the managed-host lane on every input it builds from

The lane's `paths` filters listed the entry, its manifest, two build configs,
the lockfile and the workflow itself — none of the workspace sources the
non-externalized bundle is built from. A PR touching `packages/core/src/**`,
`packages/acp-bridge/src/**` or `packages/sdk-typescript/src/**` reported this
lane `skipped`, so a break inside the bundle surfaced only when a host loaded
it. `packages/web-shell/vite.config.ts` was missing the same way even though
`vite.lib.config.ts` imports `WEB_SHELL_BUILD_TARGET` from it and uses it as
the bundle's `target`.

Both triggers now list those four inputs. The meta-test also pinned the list
with `arrayContaining`, which let two of the six entries be deleted with the
suite green; it now pins the exact set, so extending or dropping an entry is a
reviewed change rather than a silent one.

Addresses review thread R2-2 on #16200 and the same gap reported as the only
finding by the deep verification on b84f2339.

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

* fix(web-shell): keep the managed-host bundle out of lint and clean its outDir

The managed-host pass writes `dist-managed-host/`, which `packages/**/dist/**`
does not match, so a local build followed by `npm run lint` linted the 1.25 MB
bundle as source. Ignore the directory and let the managed-host pass empty its
own outDir so rebuilds stop accumulating previous hashed chunks.

Also drop the unreachable same-session workspace fallback in the pending-owner
button (`pendingElsewhere` implies `pending.sessionId !== sessionId`), and
restore the broad pending-row guard in the host dom test.

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

* fix(web-shell): close managed-host correctness gaps from review round 8

Four review findings describe defects in behavior this PR introduced; each
lands with a regression test confirmed red without its fix.

- readPending now validates the new workspaceId field like its siblings, so
  a corrupt stored record cannot hand a non-string to the host's workspace
  selection (R1-7).
- The managed page wrapper is a non-interactive focus parking spot; the
  composer's global keydown gate now treats a marked focus region like body
  focus, restoring type-anywhere after a click inside the pane (R1-11).
- A host that swaps its archived-status handler now receives the current
  status: the shell replays it when the replaced callback was stable, while
  churning inline callbacks never trigger a replay, preserving the bounded
  render behavior the frozen forwarded identity provides (R1-10).
- Slot token sync removes custom properties the shell root no longer
  defines, so a withdrawn host token cannot stay pinned inline above the
  host's cascade (R2-6).

Deferred to #16471 with recorded reasoning: mount-specific unavailable copy
(R1-5), real-page render-prop and busy-marker test pinning (R1-13, R1-14),
and a mutation trigger for host class-driven token changes (R3-2).

* fix(web-shell): read a parked focus region from the container root

The focus-region arm added for the managed page wrapper read
document.activeElement, which a shadow-root mount retargets to the host,
so clicking the pane parked focus on the wrapper while the gate still
dropped every printable keystroke. Read the active element from the
container's own root and keep the document.body arm untouched.

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

* fix(web-shell): scope a parked focus region to its own shell

Two managed shells can share one shadow root and park focus on sibling
`data-web-shell-focus-region` wrappers. The global keydown gate accepted the
attribute alone, so shell A captured a keystroke aimed at shell B and inserted
it into the wrong conversation's editor before the session submitted it.

Require the parked region to contain this composer before treating it as body
focus, and reshape both fixtures so the wrapper is an ancestor of the composer
as in the real managed DOM, with a regression witness for the sibling case.

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

* fix(web-shell): drop the unreachable provider-less identity arm

Main's `baseUrl` guard makes a provider-less render unreachable, so the
sessionId-carry-over arm that passed through one has no producer left to
pin. Prettier also collapses the i18n import.

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

* fix(web-shell): read the parked focus node from its own root (C2)

In shadow-DOM portal mode document.activeElement retargets to the shadow
host, so the page's parked-focus comparisons to body/sessions nav/discard
node were always false and the composer latch treated a click inside the
pane as a real move. Resolve the active element from the page wrapper's
own root, as App and the other focus users already do.

* test(web-shell): pin the real page as the composer focus region (C1)

The attribute plus tabIndex pair is what useComposerCore keys type-anywhere
off; the existing assertions all build their own region. Render the real
page and pin both; deleting either side reds this test.

* fix(web-shell): watch the workspace manifests in the managed-host lane (C4)

The lane builds core, acp-bridge and the SDK from source, so a change to
those workspace manifests can break the bundle while the filter reports
skipped. Add the three package.json entries to both triggers and to the
exact-set pin.

* chore(web-shell): raise the managed-host lane size baseline (C3)

Measured 7051 bytes for web-shell-managed-host.yml after C4; the baseline
still recorded 5672, so the size gate would fail on the next run.

* test(web-shell): pin the archived slot branch and the baseUrl guard (C5, R4-1)

C5: the single shared target hid the archived branch — deleting it still
left one wrapper inside the element. Use two distinct targets and assert
each gets exactly one themed wrapper.

R4-1: patrol removed the identity arm that passed through a provider-less
render because the baseUrl guard makes that render unreachable. Pin the
guard itself: a provider-less render must show the config fallback and
commit no page render, and the next provider must still render. Relaxing
the guard (or defaulting baseUrl) reds this test, so a future refactor
cannot silently bring the arm back.

* fix(web-shell): treat an unfocused shadow root as unclaimed in the discard latch

The discard focus latch read the parked node from the page wrapper's own
root, so a shadow-root mount whose discard dropped focus to the outer
document body produced ShadowRoot.activeElement === null. Every `ours`
operand was false, the latch was dropped, and the composer that just got
the draft back was never focused. Treat an unfocused shadow root as
unclaimed, matching the body-focus operand. The latch still dies when the
user moves on (ManagedSessionsPage.test.tsx pins that).

Pinned by a new shadow-root regression: the page mounts into
host.attachShadow({mode:'open'}) with a host-side navigationTarget, a
pending record is discarded, and shadow.activeElement must be the
composer. The test is red without the fix and green with it.

Addresses review thread R13-1 on #16200.

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

* fix(web-shell): resolve host-page focus before judging the discard latch unclaimed

ShadowRoot.activeElement is null both when nothing inside the root is
focused and when focus sits on a host-page element outside the root, so
the shadow-aware read alone collapsed the two states and let the discard
latch steal focus back from a user who had demonstrably moved on. Fall
through to document.activeElement when the root reports null so the
latch sees the real focus holder, and pin the host-toolbar case with a
shadow-root regression test.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-11-09 00:42:45 +08:00
易良andQwen-Coder 00aef0598a fix(ci): stop cross-job qwen-home pollution from killing PR review (#16618)
* fix(ci): stop cross-job qwen-home pollution from killing PR review

Triage's node container chowns the shared RUNNER_TEMP agent home to
uid 1000 but only restores workspace ownership at job end, leaving a
foreign-owned qwen-home behind on the persistent pool. The next review
job's cleanup rm silently fails on it and mkdir -p succeeds on the
leftover, so the run dies minutes later writing settings.json.

Restore qwen-home ownership alongside the workspace in triage, and make
the review cleanup verify the stale home is gone (sudo retry, then fail
fast with the cause named) instead of proceeding on a poisoned runner.

Fixes #16616

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

* fix(ci): make the qwen-home reclaim survive root-owned sudo leftovers

Live-runner rehearsal on hz2 showed sudo rm can delete the poisoned
home yet leave mkdir EACCES on a root-owned parent path, and a sudo
reclaim can hand back a root-owned home that mkdir -p silently accepts
— re-arming the same fail-late settings write. Fall back from sudo rm
to sudo chown plus an unprivileged rm, and verify the fresh home is
writable before proceeding.

Refs #16616

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

* fix(ci): address review findings on the qwen-home guards

Pin both guards with regression tests (reclaim, fail-fast with sudo
blocked, w+x freshness check, triage hand-back list), correct the step
header that still promised never to fail, test traverse alongside
write on the fresh home, describe the residual race the check covers,
and hand back the triage draft/result/context dirs alongside the home.

Refs #16616

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

* style(ci): prettier the new guard tests

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-08 20:42:14 +08:00
Shaojin WenandQwen-Coder 0646b57f06 fix(managed-agent): prove A3 rewritten command recovery without Hook rerun (#16116)
* fix(managed-agent): prove A3 rewritten command recovery without Hook rerun

Add the packaged command-rewrite arm and a focused three-generation witness
for two approval-bound extension command steps. Recovery keeps each rewritten
input on its own command Action and original deadline, replays settled
Hook/tool-stage results without physical rerun, and still accepts a relief
Turn and reliable close.

The real MySQL/Linux counterexample exposed small lifecycle gaps that are
fixed here: command_approval_* Action ID grammar, extension recovery resource
closure, commandStep load routing and Hook owner construction, a fast Shell
close race, and command receipt acknowledgement before Runtime retirement.

* fix(managed-agent): refuse Hook broker swap while the mount is held

delegateBrokerToMcp(broker) replaced the broker without reconciling the acquired/mount-claim state, so a config-revision enable that swapped owners after a Hook turn had acquired the mount would orphan the old claim and leave acquired pointing at the previous broker. Refuse the swap while the mount is held (the no-argument handover keeps the same broker and stays valid); a fresh load binds the new owner at construction. Adds a red/green regression plus free-swap ownership and shared-disown pins.

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

* fix(managed-agent): capture extension Shell output before an already-closed handoff

The already-closed Shell adoption resolved closure before any data listener was installed, so a command that wrote stdout/stderr and exited during owners.start()'s membership wait was persisted as a successful empty result (silent data loss) instead of the base's explicit unknown-outcome failure. start() now optionally buffers stdout/stderr synchronously from spawn (prereadOutput), and the Shell detaches, replays those bytes through its capture, and installs live listeners in one synchronous step, so no output window is lost. hook/stdio callers do not opt in and keep their fail-safe behavior. Adds a deterministic pre-handoff replay regression (red before, green after) and a real-owner spawn-buffering regression.

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

* fix(managed-agent): release preread capture on failed extension Shell startup

A failed start() never handed detachPreread to a caller and the catch left the early stdout/stderr listeners attached; since stopOwner keeps the child in the owner map, child -> listener -> buffer retained every preread byte for the instance lifetime (reproduced at 8 MiB per failed start). The catch now detaches the listeners and clears the buffers. Adds a red/green regression that injects a post-spawn journal failure and asserts the retained child has no data listeners. Also strengthens the mount-held swap guard test to construct mountWorkUnproven()=false with the mount held so the new clause itself is pinned (whole-guard removal now reds it), and adds a Linux-runnable real-spawn preread test (cgroup unit mocked) so the spawn-time attach is covered on the OS CI uses.

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

* fix(managed-agent): use ChildProcess type for the preread test cleanup handle

The Linux preread regression typed its cleanup handle as { kill: (signal?: string) => void }, whose kill parameter is wider than ChildProcess.kill(number | NodeJS.Signals). Under strictFunctionTypes the assignment of started.child is TS2322, and the test file is part of the CLI tsc --build regardless of it.skipIf, so clean builds failed on every platform. Use a type-only ChildProcess import. Verified the exact rule with an isolated strictFunctionTypes probe (old annotation TS2322, ChildProcess clean) and a full CLI tsc --noEmit (rc 0); the host OOMs on the full reference-graph tsc --build, so the bundle build is left to CI.

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

* test(managed-agent): pin failed-start preread cleanup on Linux

verify-pr round 2 F7: the failed-start preread detach/clear was pinned only by a darwin-only test, and the macOS CI lane never runs on pull_request, so removing the cleanup (M9) stayed green on Linux. Add a linux-cgroup failure regression (cgroup unit mocked, real child) that rejects the post-spawn saveOwner after a short delay so sentinels are captured, then asserts the retained child has zero data listeners; removing the detach reds it on the platform CI runs. Mirrors the reviewer's Linux probe.

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

* fix(managed-agent): address A3 review R1 findings

R1-1: replace the broker-swap guard with close-and-rebuild so a config revision bump that keeps the Hook catalog no longer parks a mount-held apply at recovery_required; revert the delegate overload. R1-2: cap the spawn-time preread at the shell 1 MiB budget with an overflow flag folded into outputCaptureLimitExceeded. R1-3: add a tool_stage domain-refusal load test. R1-4: batch the package-config retention walk (BFS + IN(...) + delta insert) with a delta witness. R1-5: precompile the Action-id Pattern at four sites. R1-6/7/8: anchor the crash/final evidence matrices and the package hook-command contract. R1-9: drop the inert admission-failure acknowledge. R1-10: read back both command-action decisions in the A3 arm. Verified by red/green regressions, CLI typecheck, the affected suites, and packaged arm owner-r46 on real MySQL 8.4/cgroup v2.

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

* test(managed-agent): pin A3 owner rebuild, preread overflow fold, broker disown

verify-pr round 3 found three test-evidence gaps where the R1 mechanism
was fixed but the regression pinned only half of it. Strengthen the tests
so each named mutation reds:
- N1: fire a second Hook after the revision bump and assert the executing
  broker owner moves ownerFor(1)->ownerFor(2); kills M-close (no-arg
  handover) and M-close2 (close without rebuild).
- N2: assert preread.overflow folds into outputCaptureLimitExceeded and
  [Output truncated]; kills deleting the fold line.
- N3: assert close() does not release the shared broker after disown;
  kills removing ownsBroker=false (double release).

Test-only; production byte-identical to 6d15829e52.

* fix(managed-agent): narrow A3 load-gate takeover and degrade pre-edge snapshots

Round-4 review (bot R2/R3 + maintainer) raised two behavioural Criticals:
- R2-1: the resume-approval gate's extensions arm lost its takeover
  conjunct, so a bare/lifecycle load of an extensions Session could set
  resume, skip the only 409 and drive a parked Turn. Require
  takeoverFlags && !lifecycle (dropping only !session.hooks so a Hook
  catalog still reaches the arm). Harness suite 693 green.
- R3-1: the bundle->contributions closure edge was enforced
  unconditionally on read, so a snapshot committed before the edge
  shipped read as corrupt. Mirror the page-envelope precedent and drop
  an absent contributions child in both the inspection walk and the
  native read; add a read-side fixture proven red/green.

Also address two Suggestions with red/green witnesses:
- R2-3: lift the A3 decision read-back into assertCommandRewriteDecision
  so the contract suite drives it; add disagreeing-revision/option cases.
- R2-6: pin that a lost durable receipt write leaves the native entry
  unacknowledged; moving acknowledge above the write reds the case.

* ci(managed-agent): cap the rewritten-command cgroup delegation step

The failover job's rewritten-command cgroup delegation was the only arm
step without timeout-minutes, so a wedged smoke could spend the whole job
ceiling uncapped. Cap it at 10 like its sibling arms and re-derive the
hosted-failover-e2e ceiling (280->290 summed, 295->305 job, allowance 15)
in both the workflow and the hosted-process-ci meta-test that pins it.

* test(managed-agent): pin the projection dedupe so lost retention reds

R2-8/R2-5: the delta-growth test could not red if the retained seed was
lost, because re-walking an already-projected level left row counts and
gmt_create unchanged. Assert the projection holds every recovery resource
exactly once across the preparing->prepared revisions; emptying the
retained seed now reds it with duplicate resource ids, proving projection
growth is bounded by the delta rather than the revision count.

* fix(managed-agent): repair merge column residue, size ratchet, native tolerance scope

Round-4 review (bot 5757389507) caught three issues introduced by the
round-4 merge / cgroup / native-tolerance changes:
- R4-1: the merge rename missed SUM(terminal) in the plain-arm durable-event
  audit; ma_public_event only declares is_terminal, so MySQL 1054 aborted the
  arm before its verdict. Restore SUM(is_terminal) and add a contract test
  reconciling every single-table ma_public_event column the runner names
  against the V1 schema (reds on terminal, passes on is_terminal).
- R4-2: the rewritten-command arm plus its capped cgroup delegation grew
  sdk-java.yml past its size ratchet; bump .size-baseline 150332->155957 the
  growth is real and the Lint & Static size gate reds without it.
- R4-3/R4-6: scope the native-snapshot contributions tolerance to transitive
  refs collected from resource bodies; a directly declared contributions ref
  stays on the strict isDeepStrictEqual path, matching the inspection walk's
  direct-reference guard instead of silently accepting a missing dependency.

* fix(managed-agent): correct the column guard tokenizer and legacy MCP retention

Round-5 review on the round-4 fixes:
- R5-1: the ma_public_event column guard was wrong in both directions — a
  quoted value (LIKE 'agent.%') posed as a column and red valid SQL, while
  ${sessionFilter} was blanked so a column typo inside it passed unseen.
  Extract a maPublicEventColumns helper that strips quoted literals and
  expands interpolated SQL fragments, with a two-direction witness.
- R2-5: the retention read-then-insert form was applied unconditionally, so
  the legacy MCP-settings path stopped releasing the projection row it
  supersedes and accumulated one row per revision for the Session's life.
  Keep the accumulate form only on the package-config branch; the legacy
  branch replaces its row. Witness: two legacy revisions leave exactly one
  SESSION_RECORD row (reds if the else branch keeps accumulating).

* fix(managed-agent): tolerate a legacy bundle's absent contributions child on commit

R6-1 (Critical, fails-closed regression): isReferenceContainer gained the
extension bundle/contributions kinds, so the commit-side closure walk descends
into a persisted legacy bundle's contributions child. That child was never
staged, so the read 404s and aborts the commit permanently — a legacy Session
loads via the read-side tolerance but can never be reconfigured. Add the
commit-side counterpart of that tolerance: drop only an absent contributions
child from the closure; a present or conflicting one stays fatal. Witness: a
commit whose closure reaches a persisted bundle whose contributions child was
never staged resolves and keeps the bundle in the census; removing the skip
reds it with 'Resource not found.'.

* fix(managed-agent): structural legacy-closure tolerance + command-step takeover arm

Round-7 review, two Criticals on the earlier tolerance/takeover fixes:
- R7-1: the legacy-snapshot tolerance was enumerated site-by-site and
  kind-by-kind, missing a dangling contributions part and a staged bundle
  whose contributions child was never uploaded. Hoist the tolerated kinds
  into one module constant (contributions + contributions-part) and apply it
  uniformly in all four walks (inspection, native page, commit persisted,
  commit staged); the staged arm gains the same async has-tolerance. Direct
  refs and present-but-conflicting children stay fatal. Two witnesses: an
  absent manifest part inspects cleanly, and a staged bundle with a 404
  contributions child commits with the bundle in the census; emptying the
  constant reds both.
- R7-2: the extensions takeover arm admitted every approval continuation, so
  an extensions+Hook Session's native approval reached resumeSavedNativeBatch's
  no-hooks refusal and 503'd the load. Scope the arm to a command-step plan;
  native approvals stay on the no-hooks arm.

* fix(managed-agent): share the extensions command-step takeover with the settle exemption

R8-1 (Critical, fails-closed): an extensions Session that also carries a Hook
catalog never satisfies `takeover` (hooks set), so the file-history settle probe
took canSettleHostedFileHistory and refused on the await_action phase alone. A
batch that owes file history while a command step parks at await_action was 409'd
on every load before the extensions resume arm was ever consulted, leaving the
parked round recoverable only by an out-of-band cancel. Compute
extensionsCommandStepTakeover once and use it in both the settle exemption and
the resume arm, so the parked command-step batch is resumed instead of demanding
a settle a parked approval can never produce. The R7-2 command-step scoping is
unchanged.

* fix(managed-agent): key both cold-load answer guards on one command-step predicate

R9-1 (Critical, regression): the second answerResumedTurnCalls suppression keyed
on resumeApproval, which is only assigned inside the takeover arm and is therefore
undefined on a bare load — so the guard was true exactly on the cold-load path it
was added to protect. A cold load answered the parked command-step round, writing
an irreversible tool_result for the plan's own skill call and permanently
destroying the continuation (or 503'ing via the uncaught hook-recovery throw).
Derive one commandStepPark predicate keyed on the unconditionally-computed
pendingCommandApproval (plus resumeApproval) and use it at both answer guards;
hoist the resumeApproval declaration above it. extensionsCommandStepTakeover is
unchanged (still requires takeoverFlags) — only the two answer guards change.

* refactor(managed-agent): extract MCP settings projection to honor the class-size ratchet

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

* fix(cli): keep the deterministic Hook owner re-acquirable across detach

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

* fix(cli): keep deterministic owners re-acquirable on every non-delete close

Move the terminal-release rule onto the owner instead of a single call site:
Hook and MCP close default to non-terminal and only DELETE opts into the
terminal release, so the shared MCP broker, the v2 lifecycle close and the
enable() owner swap no longer tombstone the id a same-revision reload
recomputes. A non-terminal close hands the mount back via releaseMount and
keeps the owner READY; the MCP drain stays ahead of the handback and skips
markReleased so the reload still derives the same owner id.

Translate the documented release-mount refusals (busy/conflict/writer-unproven)
into recovery-required on both owners instead of letting a raw rejection escape
as a close failure, and drain held Runtimes before the v1 owner close asks for
the mount back.

* fix(cli): re-initialize drained MCP configs on reload and complete the owner close fence

A non-terminal close left every mcp_configuration at 'drained', a resting state
the journal cannot reopen and nothing else consumes, so a same-revision reload
could never re-initialize MCP and stayed recovery-blocked. Advance the drained
row to released (its one legal exit) and reinstall a fresh configuration under
the same runtimeSessionId, so the server restarts, no drained row lingers, and
the deterministic owner id the reload derives does not move.

Add the documented 503 runtime_reconciliation_required release-mount refusal to
the handback fence so a Broker restart beside the Harness is named
recovery-required instead of escaping as a raw rejection that breaks the close.

Let a DELETE terminally release the deterministic owner even on a reloaded,
never-acquired Hook instance, keying the terminal arm off the terminal intent
rather than the transient mount hold, so load->detach->reload->delete no longer
orphans the owner.

* fix(cli): reinitialize released MCP configs and clear superseded drained rows on reload

The drained->released advance stranded the deterministic extension record: the
next reload resolved the same deterministic id back to the released row and
aborted. Treat a released predecessor like a drained one and reinstall a fresh
row under the same runtimeSessionId, picking a revision past every existing row.

A non-terminal close drains every row but only the latest per server is
reinstalled, so superseded same-owner drained siblings held recoveryBlocked (and
mountHandback) forever; advance them to released once the replacing active row is
committed.

The non-terminal discard sites left drained rows under a superseded owner that a
later apply's new owner made the reload reject (owners.size>1). Rather than make
the discard terminal — which would strand an apply that fails and keeps the
previous revision active — release superseded-owner drained rows at cold load,
sparing the active owner's own rows so a failed apply is never stranded.

Also sync ChildWorkspaceServiceTest's busy-storage assertion with the #16369
message reword that landed on main.

* fix(managed-agent): close the MCP/Hook owner lifecycle release gaps

Round-19 review raised three Criticals in the deterministic-owner release
path; all three are fixed:
- R15-3: the live enable callback constructed HostedMcpSession without the
  superseded-drained sweep the cold load already runs, so an off/on/off/on
  extension toggle accumulated owners.size>1 and threw recovery_required.
  Call releaseSupersededDrained before constructing on the live path too.
- R17-1: a DELETE of a never-acquired deterministic Hook owner let the
  Broker 404 (and post-restart 503 reconciliation) escape close(), wedging
  the session undeletable. Absorb both for an un-acquired owner and book it
  released, as releaseOwner does; the un-acquired terminal release itself is
  kept (R15-4). Two regression witnesses proven red/green.
- R17-2: releaseMount refused runtime_session_busy without the drain sweep
  the drain release runs, so a background Shell run outliving its turn wedged
  every detach. Sweep the unproven background rows before the busy check
  (mount handback, owner stays re-acquirable, R13-1).

hook/mcp suites 203 green; cli tsc 0; runtime-broker compiles; bundle builds.
MySQL fault-gate IT verification deferred to the CI full tier.

* fix(managed-agent): keep the mount handback observe-and-hold over live background writers

Round-20 R18-1: the R17-2 releaseMount sweep drained background rows and
then excluded exactly those rows from the busy check with no un-excluded
re-read, so a background process the sweep could not prove stopped stayed
excluded and the mount was handed back over a running writer. drain=true
also stopped legitimate background runs on the two observe-and-hold
handback routes (#13800).

Restore the contract: an observe-only sweep (drain=false) settles provable
exits and stops nothing, the excluded check clears only what settled, and
an un-excluded re-read refuses runtime_session_busy for any row still live
or unproven — the same guard the pre-R17-2 busy check already had. The
method javadoc is rewritten to match.

Witness: mountReleaseStaysBusyBehindASettledExecutionWhoseBackgroundProcessStillRuns
is green with the fix; removing the re-check reds it ("Expected
CompletionException ... nothing was thrown", releaseMount proceeds and
releaseMountCalls flips to 1). RuntimeBrokerServiceTest 199 green;
checkstyle 0 violations; spotbugs 0. MySQL fault-gate IT verification
deferred to the CI full tier.

* refactor(managed-agent): keep RuntimeBrokerService under its size ratchet

Round-21 R19-1: the R17-2/R18-1 releaseMount additions grew
RuntimeBrokerService.java to 4252 lines, past both its ratchet pin and
GLOBAL_MAX_LINES (4216), so ClassSizeRatchetTest failed and the SDK Java
lane went red. The ceilings only ratchet down, so raising them is forbidden.

Move the guarded second half of the mount handback (claim re-check, excluded
busy check, un-excluded background re-read, unproven-capture refusal, then
the transport handback) into RuntimeExecutionRecovery.releaseMountAfterSweep,
beside the observe-only sweep it follows. The facade drops to 4212 lines;
the recovery collaborator is 442 (< 800, so it needs no pin of its own). No
behaviour changes: the sweep stays drain=false, the un-excluded re-read still
refuses, and the claim re-check still runs under the lock.

Witness: ClassSizeRatchetTest reproduced the two 4252-line violations and is
green at 4212; RuntimeBrokerServiceTest 199 green; removing the re-check in
its new home reds the pinned mount test ("Expected CompletionException ...
nothing was thrown"), then restored. checkstyle 0 violations; spotbugs 0.
MySQL fault-gate IT verification deferred to the CI full tier.

* fix(cli): name a raced mount-handback admission_closed as recovery-required

Round-23 R15-2: fencedMountHandback's doc comment says its codes are the
documented release-mount refusals, and the design doc names 409
runtime_admission_closed for the close/archive/delete window
(workspace-execution-mount-recovery-block-release.md). releaseMount's
requireLifecycleRelease throws exactly that conflict when a v2 lifecycle
fence row exists and the harness is not yet draining, but the fence omitted
the code, so a non-terminal detach racing a close/archive/delete rethrew the
raw rejection out of close() and the route answered a generic
managed_session_close_failed instead of the declared recovery-required.

Add runtime_admission_closed to the 409 fence group (additive — no existing
member weakened): the lifecycle operation's own drain frees the mount, so the
detach backs off attached/retryable. Both callers (Hook close, MCP
closeResources) share the fence. Keep 404 runtime_session_not_found out (the
terminal arm books it) and do not fold in 501 workspace_mount_release_unsupported
(a permanent misconfiguration, not a retry-once-it-settles fence).

Witness: add the code to the Hook recovery-required it.each and a MCP
closeResources twin; with the fix both green (hook/mcp 208). Removing the
member reds both new cases with the raw HostedWorkspaceBrokerRejection
("Runtime Broker returned HTTP 409") instead of HostedHook/McpRecoveryRequiredError,
while the unrelated terminal-acquire admission_closed case stays green; restored.
cli tsc --noEmit 0 errors.

* fix(cli): drain held Runtimes before the v2 non-terminal Hook mount handback

Round-24 R22-1: the lifecycle-v2 settle_resources branch issued the owner
close — which for a deterministic owner on a non-terminal close hands the
Workspace mount back — before the branch's only Runtime drain. A
Hook-configured turn shares that Runtime and leaves a live background Shell
(its finish() skips the release), so the release-mount guard refused
runtime_session_busy, the fence named it recovery-required, and the route
answered 503 workspace_lifecycle_resources_unsettled on every retry while the
process lived. The drain that would have stopped the run was never reached.
The v1 close already keeps the opposite order (moved there for R14-2); the v2
branch never got the same treatment when the cleanup-debt drain landed.

Move the drain ahead of the owner close, mirroring v1. This also puts it ahead
of the publisher close, so the drain's stops land their finalize through a
still-open publisher. The owed file-history marker is already retired earlier
in the same route, so the drain's file-history guard is unaffected. DELETE is
unchanged (terminal short-circuits the handback arm), and the handback stays
non-terminal for detach with the deterministic owner id intact.

Witness: a route-level v2 settle_resources close of a session holding a
deterministic Hook owner and cleanup debt, with releaseMount refusing busy
until the descriptor drain runs — 200 with the fix, and reverting the reorder
reds it with `expected 200 "OK", got 503 "Service Unavailable"`. The first
draft of the witness was vacuous (an unrelated lease drain cleared the flag);
only the descriptor-carrying drain now counts. Full harness suite 712 green;
cli tsc --noEmit 0 errors.

* style(cli): apply Prettier to the R22-1 handback-order witness

The Lint & Static lane failed on Run Prettier: the releaseMount spy added with
the R22-1 witness exceeded the print width. Formatting only, no behaviour
change; prettier --check now passes on every file this branch touched.

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

* fix(cli): drain the held Runtime a Hook turn leaves on a v2 close

R22-1 moved the v2 drain ahead of the Hook mount handback, but the drain it
moved only acts on cleanup debt, and a Hook-configured turn writes none: it
acquires through hooks.acquire(), while oweHostedTurnCleanup and cleanupOwed
both sit in the else branch. What that turn leaves is a held Runtime recorded
client-side, and the only drain of that set was the v1 close. So the handback
still went out over a live background run, the guard refused
runtime_session_busy, and the detach parked on 503 for as long as the process
lived — the wedge R22-1 was raised for.

Drain heldRuntimeTurns in the v2 block too, mirroring v1, after the debt drain
and before hooks.close so the R22-1 order holds. Tolerate only 404
runtime_session_not_found; v1's 503 reconciliation tolerance is deliberately
not carried over because the releaseMount that follows would refuse with that
same 503 anyway, so tolerating it would only claim an unproven stop. Every
other refusal throws and the route parks honestly, as before.

The R22-1 witness was vacuous for this shape: it called f.owe() to manufacture
a descriptor the Hook path never writes. The new witness asserts there is no
cleanup debt, seeds heldRuntimeTurns instead, and keys the busy guard to the
held turn's own Runtime — the lease drain also issues {drain:true} with no
descriptor, so an opts-only check would let it clear the flag.

Red before: expected 200 "OK", got 503 "Service Unavailable". Green after.
hosted-harness-session 716/716 (full file, run alone), hook+mcp 208, build rc0,
Prettier clean.

* fix(cli): tolerate an unprovable held-Runtime drain on a v2 close

R24-1's drain tolerated only 404. The reasoning for leaving out v1's 503
tolerance was that the releaseMount right after it would refuse with the same
503 anyway, so tolerating could not unwedge the close. That premise is false:
HostedHookSession.close() guards the handback with
`ownsBroker && (mountHeld || (terminal && deterministicOwner))`, and a reload
builds a fresh instance whose mountHeld stays false until a Hook turn acquires.
After a Harness restart a non-terminal detach therefore never calls
releaseMount at all — the close would have succeeded, and the throw was the
only thing blocking it. Because the entry is deleted only on success or 404,
every retry re-seeded the same id and failed identically, leaving the mount
claimed, the lease pinned and the publishers open indefinitely.

Carry both v1 tolerances now, with the compensation that makes them honest:
503 runtime_reconciliation_required deletes the entry and marks the drain
unproven, and the two v2 monitor-loop stops settle runtime_lost rather than
stop_requested when it is. Other refusals still throw; there is no cascade to
hand those to.

Witness: a v2 settle_resources close whose held-Runtime release rejects 503
must answer 200, delete the entry, and stop the loop with runtime_lost.
Red before (expected 200, got 503), green after. Forcing stopSettle back to
stop_requested kills the test on the runtime_lost assertion, so the
compensation is pinned and not merely present.

hosted-harness-session 717/717 (full file, run alone), hook+mcp 208, build rc0
with no type errors, Prettier clean.

* fix(cli): settle a blocked v2 Session on the runtime_lost line

The v2 stopSettle added for R25-1 keyed only off heldDrainUnproven while its
comment claimed it matched the v1 close, which also folds in session.blocked.
A Session whose own settlement already failed never proved to the Runtime that
anything stopped, so certifying stop_requested there writes a terminal run
state that rebuildFromRuntimeLost refuses to recover, and reports a cancelled
task's writer as settled.

The blocked-Session half of this gap predates R25-1 — v2 hardcoded
stop_requested before it — but the parity claim in the comment did not, and
the term belongs here either way. v2 has no heldDrainFailed variable because
that arm throws instead, so the expression is session.blocked ||
heldDrainUnproven.

Witness: a v2 settle_resources close with blocked=true, no held Runtime (so
the drain is trivially proven) and an injected monitor loop must stop the loop
with runtime_lost. Red before on exactly that assertion, green after; the loop
assertion cannot pass vacuously because a failed injection fails the spy check.

* docs(cli): scope the v2 held-Runtime drain to the shapes that reach it

The R24-1 drain reads session.heldRuntimeTurns, and its comment claimed it
covered a Hook-configured turn's background run. No producer registers that
obligation for a Hook turn: finish() calls broker.release() only under
!mcp && !hooks && !cleanupOwed, runtimeHeld is assigned only in that release's
409 runtime_session_held catch, cleanup() returns early on hooks, and every
add() site keys on runtimeHeld or a cleanup hold. So the loop is empty for
that shape and the close still parks on recovery-required.

Comment-only in production. State the reachable scope (a wake turn's held
aftermath, a cleanup hold), say plainly that the Hook shape is not covered,
and record why the load restore keeps its v2 gate: a restored id can be a
deterministic Hook owner, and this drain releases terminally, which would
tombstone the id a reload recomputes.

Pin the producer fact so the drain is not mistaken for a fix of that shape:
with the same fixture and the same 409 mock, the no-hooks turn ends with
runtimeHeld true while the Hook turn never calls release and ends false.
Retitle the R24-1 case to stop claiming the Hook shape; it seeds the
obligation the way the reachable producers do.

Lifting the gap needs a keep-READY drain, deferred to a follow-up: the
terminal release used here cannot stop a deterministic owner's background
run without tombstoning it (R13-1/R14-1).

Tool-turn suite 573/573, the three v2 witnesses green, build rc0 with no type
errors, Prettier clean.

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-08 04:02:29 +00:00
lorenzozaneand易良 020df58951 fix(ci): harden the already-published desktop probe (#12184)
Co-authored-by: 易良 <[email protected]>
2026-11-08 03:07:35 +00:00
3016bf8453 fix(ci): keep sdk-java resolver bookkeeping and in-house jars out of the host cache (#15281) (#15286)
* fix(ci): scope the sdk-java test-job lock to the cache write-back (#15281)

* fix(ci): keep sdk-java SDK resolution job-local (#15281)

* test(cli): keep malformed-Unicode case titles JUnit-safe (#15281)

The parametrized malformed-Unicode cases interpolate lone surrogates into
the test title. Vitest's JUnit reporter drops a lone low surrogate together
with the preceding character — the semicolon of an &apos; entity — which
makes packages/cli/junit.xml invalid and fails the CI report publisher.

Render the case as JSON in the title and keep the raw value in the body.

* fix(ci): unlink seeded Maven metadata before the unlocked suite (#15281)

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

* fix(ci): harden the SDK Java host-cache isolation (#15281)

Widen the seeded resolver-bookkeeping unlink to every seed site and the release lane, prune pre-PR in-house coordinates inside the locked write-back, and keep the suite's exit status so a failed leg still publishes its downloads. Extend the workflow meta-tests to pin each invariant and update the bilingual design record.

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

* test(ci): pin the first mvn failure guard in the SDK Java suite-status test (#15281)

* fix(ci): harden SDK Java write-back isolation and lock verdicts (#15281)

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

* fix(ci): keep Maven resolver bookkeeping out of the shared cache (#15281)

* chore(ci): restore the size baseline final newline (#15281)

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

* test(managed-agent): align workspace upload admission probes (#15281)

The two Workspace upload routes joined the WORKSPACE_FILES rule class
without extending the acceptance probe's request builder, so the generic
JSON/empty-body request was rejected by argument binding before admission.
Send the octet-stream upload its required query parameters and the status
query its uploadId, and pin the status route's own not-found code for a
stranger.

* fix(ci): close remaining Maven cache isolation gaps

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

* fix(ci): update Maven cache cleanup contract

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

* test(ci): create runner temp for Maven seed replay

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

---------

Co-authored-by: qwen-autofix <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-11-08 02:40:39 +00:00
Shaojin Wen dfc37dffa4 fix(ci): run the failure patrol on ECS and time its sandbox image pull separately (#16462)
* fix(ci): run the failure patrol on ECS and pull the sandbox image outside the classifier budget

The patrol had effectively stopped. Of its last 20 scheduled rounds, 17 were
cancelled with zero jobs — they never received a runner at all — and one round
waited 2h09m in the queue. The single round that did run spent 304s in the
classifier, hit the 5-minute step timeout and produced no decisions, yet still
reported success: the timeout is continue-on-error and Validate only warns, so
a completely ineffective patrol looked green.

Both jobs move to the ecs-qwen pool behind the existing
MAINTAINER_ECS_RUNNER_DISABLED kill switch with a hosted fallback, using the
same routing expression as the E2E workflow. That pool had 199 idle hosts while
the patrol queued. Schedule and dispatch both check out github.sha, so there is
no fork-PR trust gate to add.

The sandbox image is resolved and pulled in its own timed step ahead of the
classifier, so the 5-minute budget belongs to the model alone. Whether the
observed 304s went to a cold image pull or to inference is NOT established; the
timing notice, reported on failure too, is what settles it. The hard-coded
0.19.9 CLI pin becomes the version the job actually runs — on the pool, the
preinstalled one — so the agent no longer forces an image the fleet does not
keep warm. Resolving the CLI is gated on the scan finding a candidate, so empty
ticks on the hosted fallback do not pay for a global npm install.

Sharing a persistent workspace required two guards. The runner allowlist is now
the first step of both jobs, ahead of the step that hands CI_BOT_PAT to the
runner, and classify preflights the docker daemon so a host without one fails
in seconds instead of burning the classifier budget. Scan clears the previous
round's decisions file and agent home: otherwise a stale file would pass
Validate and be acted on even when this round's classifier timed out without
writing anything.

The classify job cap rises from 10 to 20 minutes to cover the resolver's own
10-minute pull budget plus scan and the classifier; a lower cap would kill a
cold pull mid-flight. Cron and the classifier's 5-minute cap are unchanged, so
a worst-case cold round can still cost one tick to the concurrency group.

The workflow grows past its size allowance because the two guards are inlined,
matching how the autofix and repo-hygiene workflows already carry theirs;
.size-baseline is bumped in this commit rather than extracting a shared script
that would refactor three unrelated workflows.

* fix(ci): bound the patrol's image resolve step and pin the directory it deletes

Two findings from an adversarial pass over the previous commit.

The image resolver can attempt two sequential ten-minute pulls: the requested
image, then a fallback to the latest GHCR semver tag. The twenty-minute job cap
added in the previous commit therefore did not cover the resolve step alone,
let alone scan and the classifier, and a cold host would have been killed
mid-round with nothing produced. The resolve step now carries its own
twelve-minute cap below the job, mirroring the bound the classifier already
had, so a cold host costs one round instead of the job. The budget assertion
in the meta-test encoded a single pull and was wrong; it now requires both
bounded steps to fit inside the job cap with room left for scan.

The scan step deletes the previous round's agent home with rm -rf, and took
that directory's name from a job-level variable. On the shared pool
$GITHUB_ENV is writable by anything running at the runner's uid, which is the
threat model the image resolver's own step-file guards already encode. The
name is now pinned in the env of both steps that use it, so the destructive
command reads a step-level value, and a test binds the two spellings together
and rejects a job-level definition.

Also pins the gate shared by the CLI, image and classifier steps: if those
three conditions ever desync, the classifier runs with an empty image
reference or an empty CLI version instead of being skipped.

Five further mutations targeting only these new assertions — reordering the
CLI install ahead of the scan, dropping the resolve cap, raising it to the
whole job, drifting the two agent-home spellings apart, and desynchronizing
the classifier gate — were all caught, with the workflow restored byte-exactly
after each.

* fix(ci): close the patrol's persistent-runner credential paths and the sandbox consumer gate

Four review findings, all reproduced locally before fixing.

The always() reset step defeated the new runner allowlist. Reset hands
CI_BOT_PAT to a driver script read out of the workspace, and always() fires
even when the guard rejected the host or checkout failed — so on a persistent
runner the allowlist did not actually close the write path. The guard and
checkout now carry ids and reset requires both to have succeeded, keeping
always() so a failed act still resets state.

A reused workspace also carried executable Git metadata. The classifier's
sandbox mounts the workspace read-write and its two file tools do not exclude
.git, so a previous round needed nothing but write_file to plant a filter that
this round's checkout executes during its own clean/reset — with HEAD still
resolving to the trusted SHA, which is why a pinned ref and
persist-credentials: false do not help. Both jobs now discard .git before
checkout and let it rebuild, as triage already does, and the classifier denies
writes to .git config and hooks so the same-round post-checkout git commands
are covered too. The regression test plants a real filter and asserts the
payload fires without the rebuild, so it cannot pass vacuously.

The docker preflight had no bound of its own. A daemon that accepts the
connection and never answers hangs before the scan, where neither the resolve
nor the classifier step cap applies, so even an empty-candidate round would
hold the serialized patrol for the whole job budget. The probe is now wrapped
in a ten-second timeout under a two-minute step cap, with a test that stubs a
wedged daemon and asserts the guard returns.

Adding a resolver step also enrolled this workflow in a gate whose protected
set is derived from the tree, and that gate's consumer detector only recognized
a run: body or env.SETTINGS_JSON — not a uses: step binding
env.QWEN_SANDBOX_IMAGE. The workflow therefore contributed zero consumers and
failed the closing vacuity assert, which CI confirmed on the required Lint &
Static lane. The detector now recognizes that binding shape; no other workflow
gains a newly detected consumer, and the workflow is deliberately not exempted,
which would have disabled every binding check for it permanently.

Six mutations targeting only these fixes were all caught, including narrowing
the detector back, which reproduces the exact CI failure.

* fix(ci): make the patrol guard testable off Linux and probe the daemon it vouches for

Three review findings on the previous commit, each witnessed against the
acceptance criteria the reviewer specified.

The guard behavioural test executes the real step body, which now ends in
`timeout 10 …`, but only the wedged-daemon test carried a capability probe. On
a host without coreutils timeout the approved-runner case therefore failed
deterministically: bash reports 127, `if !` inverts it, and the guard exits 1
where the test expects 0. That suite is collected by the macOS scripts shard
and by Windows, and neither lane runs on pull_request, so the first red would
have been a merge-queue ejection. The allowlist fixture now ships a forwarding
timeout shim, which keeps those assertions live on every platform, and the
suite joins the win32 exclusion list beside its bash-driven siblings — its name
does not match the existing qwen-* glob. Witnessed as an A/B under a PATH whose
timeout exits 127: with the shim the suite is green, without it exactly one
case fails and the other eighteen still pass.

The preflight also probed a different daemon and a different binary than the
steps it vouches for. It ran a PATH-resolved `docker` with no endpoint pin,
while the classifier pins DOCKER_HOST/DOCKER_CONTEXT and the resolver spawns an
absolute path. Because $DOCKER_CONFIG/config.json is shared across pool
registrations, the guard could reject a host whose default daemon is healthy,
or pass on one whose default daemon is dead and let the classifier burn its
budget. The guard now carries the same two pins and probes through
SANDBOX_COMMAND, and a test binds both to the consumer's values so the three
cannot drift. The behavioural stub answers only for the pinned endpoint, so
dropping either pin from the workflow reds the approved-host case; witnessed,
along with the structural test.

The two new Git-metadata rebuilds were also outside the cross-workflow
symlink-safety registry, with a subset of its assertions re-implemented inline.
That registry runs the real body against a workspace whose .git is a symlink
and asserts the external target survives, which the inline copy could not
distinguish: a trailing-slash rewrite follows the link and empties the target
while `existsSync('.git') === false` still holds. Both jobs are now registered
and the duplicated structural loop is gone, keeping the planted-filter fixture
that the registry does not cover. Witnessed: the trailing-slash mutation reds
exactly the two new rows.

The wedged-daemon test gained a lower time bound as well. Pointing the probe at
a stub through SANDBOX_COMMAND means an unset variable would otherwise fail
instantly on an empty command name and satisfy the upper bound without any
timeout ever firing.

* fix(ci): pin the patrol's command lookup and neutralise global Git config

Four review findings on the previous head. Each was verified against the code
before fixing, and each fix is witnessed against the acceptance criterion its
reviewer specified.

The allowlist decided which host runs the job but said nothing about which
binary a credential-bearing step then executed. Resolve patrol identity ran a
PATH-resolved gh with CI_BOT_PAT already in its environment, and act and reset
did the same through a PATH-resolved node. This pool runs untrusted fork CI at
the same uid and the workflow itself writes a PATH directory via npm install
-g, so a co-located job can plant either name. Both jobs now capture PATH once
through expression context and every later run step exports it before its
first command word; the classifier takes it as step env, because the CLI
resolves its sandbox command as a bare name too. The capture sits after
setup-node on purpose — capturing earlier would pin the ambient node and
silently drop the version the job asked for — and identity moved below it,
which costs nothing since it needs no checkout. Validate is covered as well:
it holds no token, but a planted node there could rewrite the driver that act
then runs with the PAT.

Deleting the repository's .git also left the wider Git configuration untouched.
A core.hooksPath planted in ~/.gitconfig survives that deletion, and
actions/checkout copies $HOME/.gitconfig into its temp home before running git
checkout --force, so a planted post-checkout hook fires during the very
checkout the rebuild exists to make trustworthy and can rewrite the driver
before act runs it. Both rebuild steps now write an empty config under
RUNNER_TEMP and export GIT_CONFIG_GLOBAL and GIT_CONFIG_SYSTEM, and both
checkout steps pin the same two in step env, which outranks the appendable
$GITHUB_ENV copy. Neither touches the developer's or runner's own ~/.gitconfig.

The preflight also vouched for a binary the classifier does not use.
SANDBOX_COMMAND reaches only the image resolver; the CLI returns the bare name
docker from sandboxConfig and spawns it through PATH, so a planted docker
passed the absolute-path probe and would have run the classifier on the host
instead of in a container. The guard now refuses the round unless command -v
docker is the pinned binary. And it no longer discards the probe's own
diagnostic: a missing binary (127), a non-executable one (126) and a socket
permission failure are not hangs, and reporting all of them as a ten-second
daemon timeout pointed every round at a healthy daemon.

Two items the review recorded as deferred are fixed here rather than carried:
the job cap omitted the guard's own two minutes, so the capped steps summed to
19 of a 20-minute budget before scan and setup — raised to 25 with a test that
sums every capped step; and nothing pinned the resolver's image argument, so
dropping it would have left a green suite over a total patrol outage. The
Git-filter fixture is also isolated from the developer's global config, where
commit.gpgsign would otherwise red a test about workflow contamination.

Six mutations targeting these fixes were all caught, each hitting both the
structural and the behavioral assertion where both exist. The shared checkout
registry fixture now provides RUNNER_TEMP and GITHUB_ENV, which a real runner
always sets and the widened rebuild body needs.

* fix(ci): establish a verified execution base for the patrol on the shared pool

Root-cause fix for the fourth review round, whose findings kept regenerating
siblings because each earlier patch closed one instance rather than the shared
cause: on a persistent pool where untrusted fork CI runs at the same uid, any
name resolved through PATH and any config read from a predictable path is
attacker-controlled.

The first step now establishes an execution base using shell builtins only. A
candidate absolute path is accepted only when the job uid can write neither it
nor its containing directory; a symlink is accepted when the link's directory,
the resolved target and the target's directory are all non-writable. A blanket
symlink refusal was over-strict and a pool probe showed it rejecting node on a
correctly provisioned host. The guard publishes those paths and a system_path of
verified directories, and every later step uses them: the Git rebuild runs the
verified rm and mktemp, checkout gets only system_path, and credential-bearing
steps invoke absolute gh and node. The driver spawned a bare gh itself, so it
takes an explicit path too — pinning only the workflow's PATH would have left
that child resolving through the plantable one.

gh's own config channel is closed on every PAT-bearing step, following the
autofix preamble: pin GH_HOST, unset the enterprise and ambient tokens, and mint
a fresh GH_CONFIG_DIR per step with a failing mktemp aborting rather than
leaving gh to fall back to the shared ~/.config/gh. The gitconfig neutralisation
likewise mints an unpredictable name instead of truncating a guessable one.

The docker probe is now unconditional and goes through the verified base, and a
separate step re-checks the bare-name resolution on the exact PATH the
classifier receives, since setup-node prepends to it after the guard runs.

Two defects the pool probe exposed are fixed here as well. The classifier hit a
five-minute cap after a two-second image pull, so the model alone needs more
than five minutes: its cap is raised to ten and the job cap to thirty-five,
which covers every capped step plus the measured uncapped ones. And a round that
found candidates but produced no decisions still reported success — the exact
shape that hid twenty ineffective rounds and that the probe reproduced. A final
step now fails such a round loudly, after the evidence upload, while a round
with no candidates stays a green no-op.

Ten mutations target these controls and all ten are caught, each reverted
byte-exactly: removing or softening the verdict step, restoring either known-bad
budget, dropping the symlink branch, dropping the gh preamble, returning the
driver to a bare gh, restoring the guessable gitconfig name, removing checkout's
verified PATH, and removing the post-capture equality check. Three of those were
initially MISSED and the assertions were added before claiming coverage; the
driver's spawn had no test at all because every existing GhClient test stubs it.

* fix(ci): keep the patrol pull timing when the resolver outlives its budget

The resolve step is capped at 12 minutes, but the resolver can attempt two
sequential 10-minute pulls plus the tag fetch. In that worst case the runner
killed the step, so the elapsed-time notice never ran — losing the measurement
in precisely the slow case the separate step exists to surface. Bound the
resolver inside the step with the guard's verified timeout instead: the shell
regains control at 660s, reports the duration and exit 124, and still fails the
round. Verified on this host that coreutils timeout signals the child's process
group, so the docker pull is reaped rather than orphaned.

Also pin the ECS routing by evaluation rather than substring. `== 'true'`
contains every term the old includes() assertions looked for, so an inverted
kill switch stayed green. The routing gate now evaluates both patrol jobs for
the switch on, the switch off and a foreign repository, and the workflow suite
pins the whole expression against the canonical pool routing that gate
evaluates.

Refresh the two comments that still described the classifier's old 5-minute
budget, and bump the size baseline for the added prose (39678 -> 40452).

* test(ci): strip comments before pinning the patrol's gh reroute channel

A mutation witness on the final head commented out `export GH_HOST=github.com`
in all four token-bearing steps and the gate stayed green: the assertion used
toContain against the raw run body, which a commented-out line still satisfies.
The stage assertion in the same test already filters comment lines, so use that
idiom here too. With it, the commented-out pin reds the gate.

Found while re-running the consolidated mutation matrix; the production steps
were already correct, this only closes the hole in their regression gate.

* fix(ci): close the round-4 patrol findings at their shared root

Three Criticals, all the same class — a trust anchor that still resolved
something through a channel the step exists to distrust:

- readlink is an external command, so the builtin-only predicate was resolving
  its own trust anchor through the plantable PATH. Bootstrap it with builtins
  first and invoke it absolutely in both jobs; a planted readlink runs with
  GITHUB_OUTPUT and GITHUB_ENV in its environment and could both vouch for a
  writable symlink target and append its own output key.
- The scan resets resolved rm, mkdir and grep as bare names on a step that
  declares no PATH. A planted rm exiting 0 without deleting leaves the previous
  round's decisions file to be replayed by act, which the verdict step cannot
  catch because has_decisions is true. Bind the guard's verified absolutes and
  add mkdir and grep to its tool list; grep writes the provenance anchor.
- An unguessable git config name does not protect a LISTABLE file: a same-uid
  co-tenant can enumerate RUNNER_TEMP, match the minted name and refill it
  before git reads it. Use /dev/null for both scopes, as repo-hygiene does.

Also address the eight Suggestions: delete act's consumer-less capture step and
its misleading comment; state the real node precedence and keep the hosted
toolchain winning there, since leading with the root-owned system prefix would
fail Ensure Qwen Code's global install; pin the verdict operator and evaluate
the workflow's own expression instead of a hand-copied replica; pin PATROL_GH
and NODE_BIN on the driver-spawning steps; cap Ensure Qwen Code at 5 minutes;
make has_decisions a content verdict so an empty decision list fails the round;
and gate the POSIX-only driver spawn test off the Windows lane.

The accept path is now tested: mode bits alone reach it for a non-root uid, so
the earlier claim that it needed root-owned files was wrong. The new case reads
GITHUB_OUTPUT back, covers the symlink accept and refusal, isolates usable()'s
directory check from the readlink bootstrap's own, and plants a lying readlink.

14 targeted mutations all caught on this tree, byte-exact restore, baseline
green. 496 passed | 1 skipped across 7 suites; 134/134 shared node gates.

* fix(ci): close the round-5 patrol findings, including a macOS lane regression

R5-1 was a regression this PR introduced: three fixtures hardcoded
RM_BIN '/usr/bin/rm', which macOS does not have, and the vitest exclude is
win32-only so the macOS scripts lane collects them. Resolve rm and mktemp with
command -v instead. Witnessed against the real step body with an absent binary:
bash -e exits 127 with .git still present, bash -c exits 0 with .git present —
matching the reported shape exactly.

R5-2: git layers env-provided config above both file scopes, so neutralising
the config files left core.hooksPath reachable through GIT_CONFIG_COUNT and
GIT_CONFIG_PARAMETERS. Pin both in the checkout step's own env in both jobs, as
qwen-autofix already does.

R5-3: the actionable-round gate accepted any non-empty array while the consumer
silently dropped entries matching no candidate, so a non-empty but unmatchable
list still produced a green round that acted on nothing — the verdict step is
skipped once has_decisions is true. Close the loop in actOnDecisions: count
matches and throw when a non-empty list matched nothing. Per-decision failures
stay swallowed, and a matched-but-failed decision still counts.

R5-4: pin both guard copies, not just classify's. Dropping gh from act's loop
did not even abort, because no act step interpolates GH_BIN and the driver falls
back to a bare-name spawn. Also assert every consumed guard output is published
by that job's own loop.

R5-5: cap the capture and identity steps at 1 minute and check the output path
is a regular file before appending, so a planted FIFO cannot block them until
the job cap and conclude the round cancelled. Job cap 35 -> 36 keeps the
documented headroom now that two more steps are capped.

R5-6: gate the accept-path case on the uid alone and only the symlink arms on a
readlink -f capability probe, so macOS still pins the accept verdict and
publish()'s writes. Probed: the base accept arm invokes readlink zero times.

9 targeted mutations caught, byte-exact restore; the two not witnessable on
Linux carry the macOS-model probes above. 500 passed | 1 skipped across 7
suites, 134/134 shared node gates, size ratchet and lint clean.

* fix(ci): count actions not key matches, and stop minting a plantable gh config

Both round-6 Criticals are defects in this PR's own recent fixes.

R6-1: matched incremented as soon as the key resolved, but actOnDecision opens
with validDecision and returns silently. A list carrying all five identity
fields with a 240-char reason, rerun at low confidence, or string-typed ids
matched every key, acted on nothing, and still reported success — the exact
hollow green the throw was added to end. Validate the schema before counting
and name the rejected decision on stderr. Superseded, already-handled and
at-cap targets still count as deliberate declines, an attempted action that
threw still counts, and no_action counts because it posts a marker.

R3-2: the GH_CONFIG_DIR minted last round sits under the uid-writable, listable
$RUNNER_TEMP at 0700 owned by the job uid, and is reused by every gh call in the
step, so a config.yml written between calls captures the next Authorization
header. This is the construction the same diff rejects for the Git config sixty
lines above. Point all four steps at a path the job uid cannot create and drop
the now-unused mktemp from them and from the guard's tool list. Probed with gh:
/nonexistent/... falls back to defaults with http_unix_socket empty and creates
nothing; /dev/null/... was tried too and makes gh fail outright, so it is not used.

The planted-gh behavioural test now asserts the step leaves nothing in
RUNNER_TEMP, and deliberately still supplies MKTEMP_BIN so a regression to
minting would SUCCEED there — without that the assertion passed for the wrong
reason and only the structural pin caught it.

5 mutations caught, byte-exact restore. 503 passed | 1 skipped across 7 suites,
138/138 shared node gates, size ratchet and lint clean.

* fix(ci): declare the residue threat model and pin its channels, not the next one

Round 7's finding is not an eighth defect: against a live same-uid process no
userspace check holds, because the guard publishes its verdict through
$GITHUB_OUTPUT, which that process can append to (the runner takes the last
value for a key). Six rounds each closed one channel and found another.

So declare the model instead of implying an unbounded guarantee. The workflow
now states it defends against RESIDUE from earlier jobs — planted binaries,
config files, $GITHUB_ENV entries, uncleaned trees — and explicitly does not
claim to hold against a process running as the same uid during the job.
Closing that needs a real boundary (per-job container, dedicated uid, or an
unshared runner) and is out of scope here.

Stopgaps for the residue channels, each verified rather than assumed:

- Re-verify every published binary AT THE POINT OF USE, so a forged output
  value fails before the exec instead of running with the PAT.
- usable() and both sibling arms reject a candidate the job uid OWNS:
  ownership implies chmod, and under the residue model a uid-owned file's
  content was authored by the attacker regardless of its mode.
- The checkout overrides the REPOSITORY scope via GIT_CONFIG_KEY_*, which the
  reset cannot reach because it deletes .git and checkout re-creates it.
  Probed: with only the file scopes pinned the planted post-checkout fires;
  with these entries it does not. Also pin GIT_EXEC_PATH, GIT_TEMPLATE_DIR,
  GIT_DIR and GIT_WORK_TREE.
- Pin BASH_ENV/SHELLOPTS/LD_*/NODE_OPTIONS on every PAT step and the
  classifier, since they take effect before the body's first line.
- Pin SANDBOX and QWEN_SANDBOX on the classifier: sandboxConfig.ts returns ''
  when SANDBOX is set and lets QWEN_SANDBOX override settings, so residue
  there would switch the sandbox off and run the agent's tools on the host.
- Pin npm_config_userconfig/globalconfig, and reset QWEN_HOME again
  immediately before the classifier instead of leaving it unattended through
  the install and image pull. Job cap 36 -> 37 keeps the headroom invariant.

Two model-level tests, both with positive controls: a forged node= in the
outputs file must fail before the binary runs, and a re-planted repository
core.hooksPath must not execute during checkout.

Adding the ownership rule made the accept arm unconstructible for a same-uid
fixture, so it now runs against the real system roots and skips where the
host lacks that toolset — it skips here, and that is reported, not hidden.

8 of 9 mutations caught; the ninth (empty hooksPath override) was probed and
is a benign mutation, not a gap. 504 passed | 2 skipped, 138/138 node gates.

* fix(ci): keep the patrol's hosted fallback alive and pin its interpreter

The point-of-use recheck applied the pool's ownership/writability predicate
unconditionally, so it refused exactly the uid-owned tool-cache node the
guard's hosted branch exists to publish. Every round on ubuntu-latest aborted
at Scan, turning the emergency routing lever into a hard outage. Gate those
arms on runner.environment the way the guard gates them, and require every
declared name to be present: a blank PATROL_GH used to be skipped, which sends
the driver back to a bare `gh` lookup on the plantable PATH.

The interpreter was the one entrance the guard could not cover, because it is
chosen before the guard's first line. Pin it absolutely, and extend the same
predicate to the values nothing re-checked (rm, timeout, system_path) and to
npm/qwen, whose bare resolution decided whether to install and whose stdout
became the version pin the classifier runs. Pin the loader, proxy and trust
store channels on the node/gh consumers that lacked them.

The pre-pull needs no docker endpoint pins: every spawn inside the resolver
already goes through sandboxSpawnEnv(), verified against a rogue currentContext
and a hostile DOCKER_HOST.

* fix(ci): unset the lowercase proxy variants instead of duplicating env keys

GitHub env keys are case-insensitive, so pinning HTTPS_PROXY beside
https_proxy is a duplicate key: actionlint rejects the file and the whole
Lint & Static lane reds on it. A plain YAML parse does not see it because
the keys differ as strings, which is how it reached CI. Keep the uppercase
pins and unset the lowercase variants in the body, next to the existing
token unset — gh reads them at exec time, not at shell startup, so the
channel closes at the same point.

Add a local AST walk for case-insensitive duplicates so the class is caught
without depending on actionlint being installed, with a non-vacuity bound on
the number of env blocks it actually reaches.

* fix(ci): give npm two distinct config scopes and pin the test runner family

Pinning both npm config scopes to /dev/null makes npm abort with
`double-loading config "/dev/null" as "global", previously loaded as
"user"` before it resolves anything — even `npm --version` exits 1. That
killed the install branch, which is the entire hosted fallback, and every
step behind it. Use two distinct non-creatable paths instead, the same
construction the gh config dir already uses: away from HOME, and not
plantable because / is root-owned. A new arm runs the real npm under the
step's own values with a residue $HOME/.npmrc present.

The resolve-step and validate harnesses spread the ambient environment, so
on the ECS fleet RUNNER_ENVIRONMENT=self-hosted reached the step body and
the new point-of-use recheck refused their uid-owned stubs. That was the
required Test lane's red. Pin the hosted policy in both fixtures; the
ownership predicate keeps its own tests and is not loosened.

* fix(ci): pin the sandbox proxy command the classifier actually executes

The residue fence pinned SANDBOX and QWEN_SANDBOX but not
QWEN_SANDBOX_PROXY_COMMAND, which serve/sandbox.ts interpolates unquoted
into the proxy container's host-side command line and spawns with bash -c.
A residue value is shell syntax, not data: arbitrary commands at runner uid
inside the step holding OPENAI_API_KEY, plus a proxy network the sandbox's
HTTPS_PROXY is repointed at. The runtime refusal only fires under
tools.executionSandbox; this step sets tools.sandbox.

Also close three defects in this branch's own new tests, found while
auditing the deferred list: READLINK_BIN was executed with no point-of-use
recheck; the recheck-loop count was a floor one below the real count, so a
whole loop could vanish unnoticed; and the planted-qwen arm refused at npm
and never reached the qwen predicate, so it now drives the real predicate
on qwen alone with a hosted positive control. The npm arm reports a skip
instead of a green when npm is absent.
2026-11-08 02:01:59 +00:00
274c8a5283 fix(ci): report timed-out main CI runs instead of letting cancelled hide them (#16280)
* fix(ci): report timed-out main CI runs instead of letting cancelled hide them

main-ci-failure-issue.yml only admitted `failure` conclusions, but a job that
hits its execution cap concludes `cancelled` — so timed-out main runs never
filed an issue. That is how #16214 (the E2E docker leg rebuilding the sandbox
image past its 60-minute cap on cold mainland hosts) hid for weeks (#16272).

Admit `cancelled` at the trigger, then discriminate in the live step: a
cancelled run is reportable only when a job carries the execution-time-timeout
annotation; a plain concurrency supersede is skipped, and an unreadable
annotation fails safe toward not reporting. The download projections include
cancelled jobs so the timed-out leg is named in the report. The infra
classifier is unchanged, so a pure-timeout run (no failure jobs) files rather
than reruns, and normal failure handling is untouched.

Meta-tests: the trigger pin and routing matrix now reflect the admitted
cancelled conclusions; three new live-step tests witness timeout→report,
concurrency→skip, and unreadable→skip; the download-selector and jobs-fetch
pins are updated, as is the ci-platform-lanes trigger pin.

* fix(ci): classify cancelled runs by positive evidence, fail open when unknown

Review on #16280: real concurrency cancels carry the runner shutdown / higher-
priority / operation-canceled annotations, not just "operation was canceled",
and a timed-out job can carry a shutdown annotation alongside the timeout. The
previous discriminator only looked for the timeout text and skipped everything
else, so it rested on "absence of timeout = concurrency" and dropped unreadable
cancellations — re-creating the hiding problem #16272 fixes.

Classify by positive evidence on both sides instead: a timeout annotation
reports (even with a coexisting shutdown annotation); a concurrency annotation
with no timeout skips; neither, or unreadable jobs/annotations, fails open and
reports like the rest of the live step. Fixtures cover the shutdown and
higher-priority concurrency forms, timeout+shutdown coexistence, and the
fail-open paths; breaking the concurrency signature reddens the skip tests.

* fix(ci): address #16280 review — trust gate, renderer, successor dedup, docs

R1-4 (security): only trusted triggers (push/schedule/dispatch on main) run the
cancellation classifier; PR-triggered cancellations stay on the supersede path
so contributor-authored annotation text cannot drive the PAT-backed reporter.

R1-1: renderFailureComment now keeps cancelled jobs, so a timed-out leg is named
in the issue/PR comment even when no job concluded failure (the #16214 shape).

R1-2: factor the classifier into cancelled_class with a total_count receipt and
explicit unreadable handling; unknown fails open with a warning instead of
asserting a supersede it never established.

R1-3: a timeout-backed cancelled successor owns the older run's result (same
shared classifier), preventing a duplicate report; log wording corrected.

R1-6: the classifier runs after the stale-attempt check so a stale event is
attributed to staleness, not to a cancellation cause.

R1-7: the test stub resolves the check-run id (job id != check-run id) and
applies the caller's --jq with real jq; RUN_CONCLUSION env is pinned; fixtures
cover shutdown / higher-priority / coexisting timeout / unreadable / PR arms.

R1-5: sync the two design docs (both languages) with the new behaviour.

* fix(ci): move the cancelled-run classifier out of the workflow body

main-ci-failure-issue.yml grew to 63933 bytes, 6793 over its recorded
57140 (allowance 4096), so the workflow-size ratchet failed. Move the new
cancelled_class helper into .github/scripts/ci/classify-cancelled-run.sh,
which is what the ratchet asks for ("long steps into .github/scripts/"),
and trim review-narrative prose to the load-bearing why. Behaviour is
unchanged: both call sites invoke the script.

The workflow-shape pin that read the classifier's attempt-jobs fetch out
of the workflow now reads the script.

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

* fix(ci): do not report a cancelled run that ran no job (#16280 R1-2, R2-1, R2-4)

Critical R1-2: the classifier returned unknown (fail-open report) for a
cancelled run whose attempt has no cancelled job at all — a run displaced while
queued certifies 0==0 and reads no annotation, so it filed a "Main CI failed"
issue for a run that tested nothing. Track whether any cancelled job was seen;
none means supersede (concurrency), while unknown now only covers cancelled jobs
whose annotations are unreadable/unmatched or uncertifiable pages.

R2-1: tests for the certification false branch (partial pages -> unknown) and
the no-jobs case (-> concurrency). R2-4: a negative successor case with a real
id whose cancelled job is a supersede, exercising the == 'timeout' false branch.

* fix(ci): report a cancelled run that also carries a failed job (#16280)

R3-1 (Critical): the classifier read only `cancelled` jobs, so a run whose
job already concluded `failure` before the rest was cancelled classified as a
deliberate supersede and dropped the regression. Read `failure`/`timed_out`
jobs first and fail open on them.

R1-2 (Critical): a cancelled run whose jobs all stopped before their first
step still fell through to `unknown`, which reports. Treat "no cancelled job
ran a step" as nothing-executed, preserving the zero-job behaviour.

R3-2: give that outcome its own token and case arm, so the caller's notice no
longer claims an annotation this path never read.

R3-4: the annotation-error test now uses two cancelled jobs, because with one
the fall-through prints the same `unknown` as the guard and cannot see it
removed.

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

* test(ci): correct successor-gate comment and pin the classifier's full fetch

R3-3: the comment no longer claims the gate admits only failures. R2-4: pin the
classifier fetch including ?per_page=100 and --paginate so flag-stripping reddens.

* fix(ci): keep a failed cancel-classification read from deferring the report

The classifier printed `unknown` both for a run it read and could not match
and for a read that failed. Crediting `unknown` as the successor's own
verdict would then let a transient API failure drop the only report of a
regression, because the successor's gate may have read the same run as
concurrency and skipped. Print `unreadable` for those read failures and
keep the ownership arm on `timeout`/`unknown`.

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

* fix(ci): keep a confirmed timeout over a sibling's failed annotation read (#16280 R5-1)

The per-job annotation read exited unreadable before the verdict block, so
a sibling job's failed read buried timeout evidence another job already
carried — the same hiding this PR removes, one layer over. Remember the
failed read, keep scanning, and fall back to unreadable only when no
timeout was confirmed; a concurrency supersede is likewise not certifiable
over an unread sibling. Adds real-script regression tests (stubbed gh)
that go red on the pre-fix script.

* test(ci): exercise the successor jq projection end to end (#16280 R5-5)

The cancelled-successor deferral binds successor_id/successor_attempt from the
workflows/runs --jq projection, but every stub handed back a pre-formatted
string, so dropping the id field stayed green while production bound the
attempt number as the id. The new case returns raw JSON and applies the
caller's filter with real jq (verified red with the id field dropped), plus a
shape pin on both projected fields.

* docs(ci): align the design docs with the cancelled-run verdicts (#16280 R4-1)

The R4-1 fix widened the successor-ownership rule from `timeout` to
`timeout | unknown`, and the classifier now skips on `nothing-executed` as
well as `concurrency`, but both design docs this PR touches still described
the pre-fix rule: only a timeout-backed cancelled successor owns the result,
and a cancelled run reports only when a job carries a timeout annotation.

Restate the shipped rule in all four files (both languages): the two verdicts
that stay silent, the fail-open `unknown`/`unreadable` reports, a cancelled
run that also carries a `failure`/`timed_out` job, and the `unreadable`
successor that deliberately does not take ownership because a failed read
certifies nothing about what that run concluded.

Docs only — no workflow or script bytes change, so the workflow-size ratchet
is unaffected.

* ci: record main's 501-byte growth of main-ci-failure-issue.yml (#16280)

Merging main into this branch brought in 7cb46d9f (#13969), which added a
seven-line comment — 501 bytes — to main-ci-failure-issue.yml. The merged file
is 61381 bytes, 145 past the 57140 + 4096 ratchet, so the workflow-size gate
fails on growth this PR did not author.

This PR's own contribution to that file is untouched: the merge's diff on it is
exactly main's seven added lines and nothing else. Record the merged size so
the ratchet reflects the tree this branch now carries.

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-11-07 23:59:26 +00:00
f8256e8222 fix(release): derive the next patch when the preview baseline lags latest (#16470)
* fix(release): derive the next patch when the preview baseline lags latest (#16466)

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

* fix(release): derive the stable bump from the rollback-aware latest (#16466)

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

* refactor(release): share the published-latest bump helper (#16466)

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

* fix(release): refuse cross-line stable versions from older refs (#16466)

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

* fix(release): refuse stable releases from refs behind latest (#16466)

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

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

* fix(release): refuse cross-line and stale stable derivations (#16466)

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

* test(release): pin the cross-line guard's strict boundary (#16466)

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

* fix(release): refuse a derived base above the checked-out ref (#16466)

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

* fix(release): keep same-line next-patch promotions allowed (#16466)

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

---------

Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-07 22:43:17 +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
Shaojin WenandQwen-Coder cf29f90062 fix(ci): route the review/resolve permission gate to the ECS pool (#16482)
* fix(ci): route the review/resolve permission gate to the ECS pool

The authorize job gates every @qwen-code /review and /resolve command, but its
ECS routing required github.event.pull_request, which issue_comment and
workflow_dispatch payloads do not carry. Every command therefore fell through
to ubuntu-latest, and the gate sat in the hosted queue for hours (PR #15286:
3h10m for a 4-second job) while the ECS pool was idle.

Route issue_comment and workflow_dispatch to the persistent ECS pool, matching
qwen-autofix.yml's route/review-scan pattern for PAT-bearing API-only steps.
authorize runs no agent, checks out nothing, and processes no PR content, so
this does not widen the fork attack surface. PR-payload events keep the
same-repo fork guard; the hosted fallback and ECS kill switch are unchanged.

resolve-pr and publish-resolution stay on hosted by design (ephemerality); this
removes the gate wait only.

* fix(ci): harden the authorize ECS-routing pins and risk docs

Address review feedback on the gate routing change:
- Pin the checkout-free invariant the ECS routing depends on: a uses: step on
  authorize would execute contributor-authored files on the persistent pool
  with CI_BOT_PAT in the step env while the expression pin stayed green (R1-1).
  Scoped to authorize; mutation-witnessed (adding actions/checkout reds it).
- Record the trigger-set widening in the design-doc Risks (both languages):
  issue_comment makes the gate fireable by any GitHub user on a fork PR, and
  the should_review authorization fact is now produced on the shared host and
  consumed downstream without re-deriving (R1-2).
- Correct the precedent wording: route loads only the read-only github.token;
  review-scan is the job that loads CI_DEV_BOT_PAT on the pool.

* fix(ci): harden the shared-pool authorize gate against gh config reroute

Address the round-2 CHANGES_REQUESTED review (R2-1 Critical, R2-2):

R2-1: routing the gate to the persistent ecs-qwen pool moved its CI_BOT_PAT
gh api call onto a host with a shared, writable ~/.config/gh. A co-resident
survivor could plant http_unix_socket there, receive the PAT, and forge the
permission response downstream consumes as should_review. Prepend the same
pins as qwen-autofix.yml's route/review-scan — export GH_HOST=github.com plus
a private, guarded GH_CONFIG_DIR (a failed mktemp exits 1; the gate fails
closed). No `unset GH_TOKEN`: it is this step's only credential. Witnessed
with real gh 2.103.0: the as-written step leaks the PAT to a planted socket
and approves a stranger; the hardened step denies with the socket untouched.

R2-2: extend the checkout-free guard to the run: half — pin the step
inventory to the single permission step and forbid `${{` spliced into any run
body (event values must arrive via env:). Mutation-witnessed: removing the
preamble and adding a run-only second step each red the test.

Also correct the design-doc parity claim (both languages): the exposure
matches review-scan only WITH these gh pins; document the hardening.

* fix(ci): extend the ECS gate routing to triage and harden the gate pins

Two batches, verified together before push:

1. Extend the command-gate routing to qwen-triage.yml's authorize (the
   /triage, /tmux, /verify sibling blind spot): the same issue_comment /
   workflow_dispatch disjuncts, and — because that job has three steps that
   spend CI_BOT_PAT on gh — the same self-contained GH_HOST + guarded
   GH_CONFIG_DIR preamble on each, per the repo's per-step convention. Raise
   the recorded workflow size baseline to match the added hardening.

2. Address round-3 review (R2-2 fix-induced, R3-1): the gate pins tested
   substring presence only. Now evaluate the comment-stripped live lines and
   pin the guarded mktemp form + fail-closed shape, require GH_HOST /
   GH_CONFIG_DIR exported before the first gh api, and forbid executing
   repository code via relative paths or interpreters (the pool cwd is the
   reused $GITHUB_WORKSPACE). Applied to both the review and triage gate
   tests. Mutation-witnessed: an unguarded one-liner, a relocated preamble,
   and an appended `node ./…` line each red the tests.

Tests: qwen-pr-review-workflow.test.js 271, qwen-triage-workflow.test.mjs 131,
ci-runner-routing.test.mjs 103 — all pass; prettier + ESLint clean.

* fix(ci): supply RUNNER_TEMP to the triage gate replay tests; harden the gate pins

R4-1 (Critical): the new ${RUNNER_TEMP} gh-config preamble is the first
RUNNER_TEMP reference in the triage permission step, and three replay
harnesses in scripts/tests/qwen-triage-workflow.test.js execute that body
verbatim without setting RUNNER_TEMP — off-CI (npm run test:scripts) the
fail-closed guard exits 1 before writing should_run. Set RUNNER_TEMP to each
harness's existing mkdtempSync dir (the :1773 precedent); the workflow's
fail-closed guard is unchanged. Verified: the file is red under
`env -u RUNNER_TEMP` before and 203/203 green after; dropping the line reds it
again. (The review named two harnesses; running the full file surfaced a third.)

Also clear the round-4 deferred notes:
- R4-2: the /verify ack comment no longer promises a receipt "even when the
  sandbox lane is backed up" — the gate now also queues with the pool.
- R4-3/R4-4: the triage routing pin is now an exact expression equality, the
  step inventory is pinned, and PAT-bearing steps are identified by env
  GH_TOKEN rather than gh subcommand spelling.
- R3-1: tighten the no-repository-code deny-list (quote boundaries +
  interpreter as a word) so `bash -c './x.sh'` no longer slips past;
  mutation-witnessed in both the review and triage gate tests.

Tests: triage vitest 203 (off-CI), triage .mjs 131, review vitest 271,
ci-runner-routing 103 — all pass; prettier + ESLint clean; size gate green.

* fix(ci): correct the gate-routing narrative; close the round-5 verify gaps

R5-1 / F1 (narrative): the motivating incident is a /resolve, whose
end-to-end latency this PR does not change — resolve-pr stays on
ubuntu-latest, so only the gate wait is removed. State this in both
design-doc languages and the pinning test comment; the saving lands on
/review, /triage, /tmux, /verify (pool-routed downstream). Correct the
"cancelled without ever running" sentence to the run's timeline (resolve-pr
started when the gate finished and was cancelled ~29m36s later).

S1: the no-repository-code deny-list missed absolute-path interpreters
(/usr/bin/node x.js) and versioned Python (python3.11 x.py); widen the regex
(optional path prefix + python[0-9.]*) in both gate tests. Mutation-witnessed:
both previously-surviving mutants now red, clean tree green.

S2: each hardened gh step minted a $RUNNER_TEMP/*gh-config.* dir that nothing
removed; add trap 'rm -rf "$GH_CONFIG_DIR"' EXIT to all four steps (RUNNER_TEMP
survives between jobs on the pool).

S3: record the exact final workflow sizes in .size-baseline.

Tests: triage vitest 203 (off-CI, RUNNER_TEMP unset), review vitest 271,
triage .mjs 131, ci-runner-routing 103 — all pass; prettier + ESLint clean;
size gate green.

* fix(ci): pin the no-repository-code invariant on every triage gate step

The triage gate meta-test applied the relative-path/interpreter denylist only to PAT-bearing steps, so a repository-code execution line added to the credential-free budget step survived the suite (witnessed by mutation). The no-repository-code invariant is a job-wide safety premise of the ECS routing: even a credential-free step that runs a planted file leaves a survivor on the persistent host. Pin it on every step's comment-stripped live lines, and align both design-doc versions with the widened coverage and the witnessed mutation set.

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

* docs(ci): correct the routing-matrix coverage claim for the triage gate

The validation plan said the triage gate is pinned by the triage meta-test AND the ci-runner-routing matrix, but the matrix never reads either authorize job (it pins the sibling pool jobs). State the actual coverage in both language versions.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-07 13:47:42 +00:00
4d61487419 ci(release): only file issues on failure and reconcile stale ones (#15852)
* ci(release): file failure issues only for failures and close stale ones

The VSCode companion and Chrome extension notifiers filed an issue when a job
was merely cancelled, and nothing closed that issue once a later attempt
shipped, leaving a live failure report against a released version.

Drop the cancelled arm from both notifiers and add a success-gated
reconcile-failure job to each, reusing the shared release step. That step now
takes the title prefix and release version so all three release workflows can
close the bot-authored issue an earlier attempt filed.

Fixes #15840

* test(release): pin the version boundary when closing a stale issue

A nightly sibling of the same release sorts first in the gh issue list result, so dropping the " on " suffix from the title guard silently closes another release's issue. The fixture now covers that ordering and the boundary mutant is killed.

* test(release): tie each reconcile prefix to the title its notifier files

The fan-out suites asserted `FAILURE_TITLE_PREFIX` against a hand-written
constant, so renaming the notifier's `--title` left both green while the
real reconcile would match no issue and the stale release-failure issue
would stay open against a shipped release (QwenLM/qwen-code#15852).

Read the prefix out of the notifier's own `--title` argument and assert it
starts with the prefix the reconcile job passes, instead of restating the
literal a third time.

* test(ci): pin the version token and separator in the release title contract

reconcile-failure reads a notifier title through startswith(prefix + version
+ " on "), but the suite only pinned the prefix. A notifier that dropped
the version or separated it with something other than " on " closed
nothing while both suites stayed green, which is a silent failure of the
stale-issue cleanup (#15840).

Derive the rest of the title from the notifier's own --title, accept the
version variable with or without its :-unknown default, and assert the
reconcile job reads the same variable the notifier writes.

---------

Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: 2SinceJuly <[email protected]>
2026-11-07 12:52:38 +00:00
Shaojin WenandQwen-Coder a12d6946c2 fix(managed-agent): mask digit-led passwords and refuse an empty Harness user info in the upgrade command (#16352)
* fix(managed-agent): mask digit-led passwords and refuse an empty Harness user info

Follow-ups to the review items deferred on #16110 and #16175:

- A refusal hides everything up to a URL's last @, also when the URL
  parses: a password that starts with digits and holds a / ? or # reads
  as a port to both the URL parser and java.net.URI, so
  http://svc:2024#Secret@harness:4170 printed whole. The --server-url
  query refusal masks too.
- Without the server's verdict, start refuses an @ before the Harness
  host (http://@host, http://:@host), which java.net.URI reads as user
  information and the connector refuses, instead of restarting the
  Harness for it.
- The health row and start show a status of 0, false or "" instead of
  dropping it, and print the URLs they dial through the control-byte
  neutralizer.
- A Session id with a lone surrogate fails its row by name instead of
  "URI malformed".
- The Flyway reader test covers a path two modules both ship; the user
  guide names verify --tenant's --actor condition, and the design docs
  state the masking, status and id rules as applied.

* fix(managed-agent): neutralize the DEL and C1 bytes a quoted value keeps

JSON.stringify escapes C0 controls but not DEL or C1 bytes, so a refused
URL or a Session id the verify row echoes could carry U+009B, an 8-bit
CSI introducer, to the terminal, and the id was not bounded. The four
refusals that quote a URL and the id row now go through printable()
after JSON.stringify: C0 controls keep their escapes, DEL and C1 bytes
show as U+FFFD, and the quoted text stops at 200 code points.

* fix(managed-agent): print no password a --server-url carries, and stop predicting java.net.URI

Review round 2 on #16352.

- A --server-url with an @ left after the URL parser reads no user
  information is refused: it is the rest of a password that starts with
  digits, which the parser reads as a host and a port, so the phase would
  dial the wrong host and print the password. check judges --server-url
  the same way, since its plan repeats it.
- start's three outcomes and verify's health row print every URL with its
  user information masked.
- With the server's verdict false, the refusal names a reason only where
  the value shows one the server gives too: a user name or password, a
  query or a fragment, or an @ in an authority after //.
- Without a verdict, start takes only a plain http(s) URL, a rule of its
  own and narrower than the server's, instead of modelling how
  java.net.URI reads user information: http:/@host, a backslash, a tab,
  an underscore or a space no longer reach the Harness restart, and an @
  in a path still does.
- The design record and the deployment guide say so.

* fix(managed-agent): correct upgrade URL guards and plan output

* fix(managed-agent): keep refusing an @ in --server-url, and spell plan words

affe6c5de3 dropped the refusal of a --server-url with an @ left after
the URL parser read no user information, to let a path hold one. That
is the shape of a password that starts with digits: for
https://svc:12/[email protected] the parser reads host svc and port
12, so start and verify dialled svc with the password in the path. The
refusal is back; an @ a path holds is written %40, which the docs now
say, and a test pins a proxy prefix spelled that way.

The plan printed --server-url through shown(), which turns a control
character into U+FFFD and masks the text before an @, so the printed
command was not the one to run. check refuses such a value before the
plan, and shellWord now spells any control character in any plan word
as the escapes of its UTF-8 bytes inside $'...', which bash reads back
to the value, instead of leaving it raw inside quotes.

The Harness rule start applies without the server's verdict follows
java.net.URI's host grammar: no empty label, no label that ends in a
hyphen, a last label that starts with a letter, a dotted-quad IPv4
address, and an IPv6 address of the standard forms. A differential run
of 2,000,000 generated URLs found none the rule takes that
java.net.URI refuses.

The usage line says what check does with --server-url without
suggesting it dials it.

* fix(managed-agent): accept trailing-dot Harness hostnames

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

* fix(managed-agent): take the IPv6 Harness hosts that end in IPv4 when the configuration does not resolve

The plain-URL rule start falls back to refused an IPv6 address that ends in
an IPv4 one, such as http://[::ffff:127.0.0.1]:4170, which the server's
Harness connector accepts, so the same Harness URL was refused while the
configuration did not resolve and taken once it did; the previous commit
took the final dot of a fully qualified name for the same reason. The rule
stays narrower than java.net.URI's, never wider: it still refuses a one-label
name that starts with a digit and an octet with a leading zero, which the URL
parser fetch uses reads as another address, and an empty port.

The refusal of an @ in --server-url now names its remedy, %40, as its sibling
refusals do, and the comment that describes quoted() sits above it.

* fix(managed-agent): tighten upgrade URL and plan handling

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

* fix(managed-agent): harden upgrade rows and Harness URL checks

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

* fix(managed-agent): take the digit-led Harness hosts fetch reads alike, and refuse an empty --server-url

The no-verdict Harness rule refused a one-label name that starts with a
digit, such as a container's 4harness, which java.net.URI and fetch both
read as that host. It now refuses only what the URL parser fetch uses
reads as another address: a one-label number, in decimal or 0x hex, and
an octet with a leading zero. The last label of a longer name still
starts with a letter, as java.net.URI requires, so harness.1b stays
refused, and an empty port is taken. The refusal says what the rule
needs instead of "a host and port with a path".

check, start and verify test whether --server-url was given rather than
whether it is truthy, so the empty value an unset variable leaves is
refused instead of printed into the plan and then ignored.

A plan's control bytes are written as three-digit octal escapes inside
$'...', which bash, zsh and ksh all read back; ksh read a \x escape on
through a hex digit after it.

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-07 17:43:51 +08:00
jianyx1 222ca313e2 test(ci): harden TUI workspace reconciliation (#16033) 2026-11-07 17:42:14 +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
Shaojin WenandQwen-Coder f387c3a75a fix(ci): recreate git metadata before the publish-triage checkout (#16494)
* docs(ci): state the real publish-triage safety argument

The publish-triage header comment claimed a 'clean runner', but the job routes to the persistent ECS pool like publish-tmux and runs without a container. The guarantee that actually holds is trusted-code-only: a sparse checkout pinned to github.sha, never agent-provided code, and a publisher that imports only node builtins plus one relative helper, so a reused workspace offers no bare-specifier node_modules to hijack. Reword the comment to say that, per the non-blocking note in the #16468 re-review. Comment-only change; no job, timeout, or behavior delta.

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

* fix(ci): recreate git metadata before the publish-triage checkout

The patrol closeout on the comment rewrite checked the claims against the file and found two gaps. First, publish-triage was the only job reaching actions/checkout on the shared pool without recreating the workspace .git: an earlier agent run owned that workspace and could have planted hooks/config that checkout's git commands would execute in the one job holding the write PAT. Add the same Recreate runner Git metadata step the triage/tmux/verify jobs run before their checkouts, and add publish-triage to the qwen-runner-checkout-workflow matrix that behaviorally executes it (removal witness: deleting the step turns the new entry red). Second, narrow the header: drop the 'it is safe there' certification in favor of naming the controls, and replace 'sole writer' — the triage job's reaction and lifecycle host steps also write with the PAT — with 'the only path from the agent's drafts to GitHub'. Design pair updated in both languages.

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

* docs(ci): keep the new step comment off the PAT-holder claim

The publish-triage step comment and the design pair said the checkout runs in
"the job that holds the write PAT", which is the same overstatement this PR
removes: the triage job's reaction, status comment, and finalize step also
write with that PAT. Name the publisher as what it is instead, so the header's
new parenthetical and the design prose agree.

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

* fix(ci): pin the publish-triage safety claims and derive the checkout guard

Addresses the remaining review findings in this PR. R1-2: publish-tmux's banner still claimed a clean GitHub-hosted runner while its runs-on selects the ECS pool; reword it to the controls that actually hold (checks out nothing, runs no repository code), routing untouched. R1-4: the publisher's bare-specifier-free import closure is load-bearing for the pool argument but nothing pinned it; add a meta-test that scans the closure and matches it against the sparse-checkout list (witness: adding import 'yaml' reddens exactly that test). R2-2: the runner-checkout guard's hand-typed job list is what missed publish-triage; derive the qwen-triage entries from pool-routed jobs that check out, so the next such job reddens with no test edit (witnesses: a new pool checkout job without the reset, and deleting publish-triage's reset, both redden). R2-3: pin publish-triage's ownership-restore fallback chain with assertUnconditional (witness: deleting the sudo fallback reddens). R2-1: the design pair's Files-affected list still said the runner-checkout test needs no change; correct both languages and narrow the pinned-by parenthetical to what each suite actually pins.

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

* test(ci): make the new pins layout-independent, floored, and complete

Round 3 found three holes in the pins added by the previous commit, all in the guards themselves. R3-1: specifiersOf used a single-line pattern, so Prettier-wrapped imports were invisible in both directions — a wrapped bare specifier kept the closure test green while the pool job would die ERR_MODULE_NOT_FOUND, and a wrapped relative one dropped a needed entry from the expected sparse list. Span newlines, hoist the scanner to module scope, and fixture-pin it (wrapped relative, wrapped bare, and the publisher's own multi-line node:fs import); those three fixtures were verified red against the old pattern and green after, and the sparse comparison stays exact equality. R3-2: the derived job set had no floor, so rewriting one pool job's runs-on into a vars lookup silently dropped its row; add an arrayContaining floor that names the lost job while still admitting new pool jobs with no test edit. R3-3: the ownership test's name promised the full fallback chain but assertUnconditional matches chmod unanchored, so deleting the sudo chmod arm stayed green; pin that arm in this test rather than the shared helper, which has five other call sites. Witnesses: wrapped bare import reddens the closure test, a vars-lookup runs-on reddens the floor naming publish-triage, and deleting the sudo chmod arm reddens the ownership pin — each with byte-level restore.

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

* test(ci): prove the sparse checkout by loading it, not by scanning syntax

The APPROVED review deferred three findings against the closure guard, all instances of one root cause: the guard modelled JS module syntax with a regex. Proven on the shipped code — a 1164-character wrapped bare import left it green (the 400-char bound), and an out-of-tree helper turned the byte-offset slice into the garbage entry /.github/scripts/elper.mjs. Replace the scanner with the condition the pool actually creates: materialize exactly the sparse-checkout list into a temp tree, assert no node_modules exists above it, and import the publisher there. Sufficiency is the load succeeding; necessity is each .mjs entry omitted making it fail; the owner policy is tied by resolving the publisher's own new URL literal and requiring that exact path in the list, instead of an unanchored substring plus a hard-coded entry. This removes the scanner and its layout fixtures rather than patching them again — wrapped imports of any length, prose that looks like an import, and out-of-tree helpers are all covered by the load itself. Witnesses: the long wrapped bare import that was previously green now fails, as do a short bare import, an out-of-tree helper, a helper dropped from the list, a stale extra entry, and a moved owner-policy path; each with byte-level restore.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-07 15:21:36 +08:00
e99286813e fix(ci): preserve Maven suite results across cache write-back failures (#15274)
* fix(ci): scope the sdk-java test lock to the cache write-back (#15262)

The Managed Agent fast tier can fail without reporting a test result after waiting out the per-host SDK Java lock. #15128 scoped the MariaDB lane's lock to its cache write-back, but the test job still held the same lock across the SDK and Managed Agent Maven suites, so a long full-tier suite could starve the fast tier on that host.

Seed the test job's Maven repository from the host cache, run both suites unlocked from that job-local copy, and take the ticketed host lock only for the cache write-back. The write-back children close fd 9 so a cancelled job's survivor cannot retain the lock. Pin the new order and the write-back fd closures in the workflow tests, and update the fair-lock design docs and the workflow size baseline.

Fixes #15262

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

* fix(ci): keep sdk-java cache write-back best-effort (#15262)

* fix(ci): keep the sdk-java gate result across the cache write-back (#15262)

The write-back runs after the suite, so the step's exit code must come from
the suite: both lanes now track the Maven status, demote only a lock-wait
timeout with a step-summary record, and redden when the queue itself is
unusable. The wait drops to 1800 s because it guards only the bounded copy;
the seed stops hiding cp's diagnosis while staying quiet on a fresh host;
the design docs record the head-of-queue stall the shorter hold does not
bound.

* fix(ci): revert the sdk-java lock helper to the PR footprint (#15262)

The previous round changed .github/scripts/sdk-java-lock.sh, which this PR never touched; the deterministic gate rejects that expansion. Restore the helper and keep both call sites on the pre-round `if !` shape, retaining the 1800 s wait and the step-summary record. Drop the status-2 pins and tests that only existed for the reverted helper, and record the unusable-queue gap as a follow-up in the design docs. The timeout-returns test stays.

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

* fix(ci): de-alias every resolver tracking file and guard an unusable lock queue (#15262)

* fix(ci): de-alias Maven install metadata in the sdk-java seed (#15262)

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

* test(ci): restore the per-job Maven seed binding (#15262)

* fix(ci): keep sdk-java resolution metadata out of the host cache (#15262)

The seed drops maven-metadata-*.xml hardlinks, but the write-back still
published job-local metadata with cp -n, where the first publisher's copy
could never be refreshed. Exclude it at both write-back sites and pin the
seed and write-back name sets to each other.

Also witness each suite-status exit with an exit-1 case, restore the lock
test's combined-stream assertion, and bring the design docs' acceptance
criteria and fault arms up to date.

* test(ci): carry the deferred-status helper into the sdk-java slices

The tolerated-skip arms now call mvn_report_failed_suite, which the step
defines above the lock block. Every slice that starts at lock_setup_fault()
or at the write-back therefore ran bash -e without the definition and died
with 127, which read as a code failure rather than a missing prelude.

* docs(ci): clarify remaining sdk-java cache hardening scope

* fix(ci): bound failed MySQL cache waits and exclude resolver residue

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

* fix(ci): clear inherited resolver state in the Maven cache seed prune

The seed de-alias prunes carried -links +1, which restricted deletion to
the hardlinks cp -aln created in this job. RUNNER_TEMP survives across
jobs on the pool, so a previous job's mutable resolver state (nlink 1
*.lastUpdated, _remote.repositories, resolver-status.properties) survived
in the job-local repository and could redden an otherwise green Maven
lane. Drop -links +1 from all four seed prunes so inherited state is
cleared again; the write-back's -links 1 delta filter is unchanged and
the seed stays best-effort (2>/dev/null || true).

Move the deAlias test pin with the fix and add a regression witness that
plants single-link markers in the job-local repository before the seed
body runs and asserts they are gone (red with -links +1 restored, exit 1;
green with the fix, on macOS and Linux). Re-record the sdk-java.yml
.size-baseline entry (153970 bytes) and synchronize both design-doc
languages, including the nine-coordinate set the main merge brought in.

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Qwen Autofix <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-11-07 08:51:30 +08:00
Shaojin WenandQwen-Coder dd4515ca27 feat(ci): isolate triage agent credentials and run all triage on ECS (#16468)
* feat(ci): isolate triage agent credentials and run all triage on ECS

The triage agent carried a write PAT, so comment reruns and external fork-PR triage had to stay on ephemeral hosted runners while hosted capacity is tight and the ECS pool has headroom. Wire the token-isolation follow-up the workflow already documented: the agent reads GitHub with the job's read-only GITHUB_TOKEN and expresses every write as a draft; a separate publish job validates and executes drafts with the bot PAT, using one publisher script shared with local /triage runs. Contain the agent job, launch the CLI directly behind the hardened loopback model proxy, and move the deterministic maintainer resolver into the publisher. With no write credential in the agent environment, route every triage trigger to ECS behind the existing kill switch, and retire author_can_write. Markers, bot identity, and the finalize workflow's approve-on-green protocol are unchanged.

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

* fix(ci): bump triage CI budget and address triage review findings

The new publish-triage job crossed the .ci-budget ratchet (7->8 jobs, 280->290 declared timeout-minutes), aborting Lint & Static before any lint or helper test ran. Bump the budget entry, mirror the artifact-name fallback expression on the upload side so an early-death run's drafts stay addressable, and enforce SKILL.md's process-label rule at the publisher chokepoint (welcome-pr, maintainer, help wanted, good first issue) with fail-closed tests, aligning the issue-workflow instruction that previously contradicted the rule.

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

* fix(ci): harden the triage draft pipeline after an adversarial audit

Six findings from a multi-round pre-push audit, each with a mutation witness: kill lingering agent-user processes before the drafts strip/upload so a detached child cannot re-plant symlinks in the race window (upload-artifact follows them and RUNNER_TEMP is host-shared; unkillable survivors drop the draft set deny-by-default); lstat the draft containers themselves in the publisher preflight, since a symlinked bodies/ redirects the realpath containment base; reject cross-kind actions (review/close-pr on issues, close-issue on PRs); install util-linux for runuser as the sibling lanes do; make the mktemp'd proxy-wrapper directory traversable by the agent user so the gh/git wrappers do not silently fall out of its PATH; pin the artifact-name symmetry, the kill-before-strip ordering, and the toolchain install line. Design pair updated in both languages.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-06 18:17:45 +00: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
Shaojin Wen 478c2f87ab feat(ci): make flaky-patrol rerun and no_action decisions visible (#16459)
* feat(ci): make flaky-patrol rerun and no_action decisions visible

The CI Failure Patrol posted only a hidden HTML marker when it re-ran a
flaky failure or could not classify one, so maintainers saw a red PR with
no explanation and clicked Update Branch — advancing the head, dismissing
approvals, and re-running the whole CI for a failure that was already
being handled on the same commit.

Post a short bilingual notice alongside the marker for both decisions:
the rerun notice states the transient cause and that no Update Branch is
needed; the no_action notice asks for manual review before rerunning or
updating the branch. The deterministic-failure comment is byte-identical,
and the post-success reset marker stays hidden.

Pin the visible notices, the marker round-trip through dedup and the
per-head action count, and reason escaping for all three actions.

* fix(ci): strip bidi overrides from flaky-patrol notice reasons

An adversarial audit of the visible patrol notices found that bidi
control characters (U+200E/U+200F, U+202A-U+202E, U+2066-U+2069) passed
through the reason escaping untouched. The reason is classifier output
derived from untrusted CI logs, and a right-to-left override can visually
rewrite the very sentence maintainers trust when deciding whether to
update a branch.

Drop those characters in the shared escaping helper so all three visible
decisions (and the deflake issue signature) are covered, and pin the
stripping for each action. Removing the strip line fails the new test.

* fix(ci): cover the Arabic letter mark in the patrol bidi strip

An escalated audit round compared the strip class against the full
Unicode Bidi_Control set and found U+061C (Arabic letter mark) missing;
it can reorder adjacent Arabic-script text in a visible notice. Add it
to the class and extend the per-action pin payload, so the class now
matches the complete Bidi_Control set. Removing just U+061C fails the
pin test.

* fix(ci): collapse newlines in patrol reasons and pin the audit gaps

The verification round found that a newline plus a line-start fence in a
classifier reason opens a block-level code fence in the visible notice:
the folded Chinese block and the tracking marker render as visible code.
Collapse newlines and their surrounding whitespace to a single space in
the shared escaping helper, keeping every reason single-line.

Close the coverage gaps the same round measured: feed all twelve
Bidi_Control code points individually, pin the English-before-fold and
Chinese-inside-fold placement of the deterministic comment, and assert
the fold's closing tag in all three notices.

Witnesses: removing the collapse, narrowing the bidi class, dropping
U+200F alone, swapping EN/ZH, or dropping the closing tag each fails the
suite; a marked rendering probe shows the fence swallowing the fold
before the fix and a clean render after; benign comment bodies remain
byte-identical to base.
2026-11-06 23:10:16 +08:00
liuxuely0626andluoyun ee1a9b1e1e fix(ci): move audio prebuilds to macOS 15 (#16464)
Co-authored-by: luoyun <[email protected]>
2026-11-06 22:47:15 +08:00
7cb46d9fed feat(hooks): add FileChanged notifications for local sessions (#13969)
* feat(hooks): add FileChanged notifications for local sessions

* fix(ci): preserve shell gate coverage and trim transcript data

* fix(ci): flush debug prompts and isolate git fixtures

* fix(hooks): respect ACP watcher ownership and skill registration

* test: stabilize hosted recovery and failed spawn fixtures

* fix(ci): budget the complete hosted integration fork

* fix(runtime-broker): accept MariaDB CSI guard metadata

* test: align schema baseline and hook dispatch waits

* fix(ci): allow hosted shared fork to finish all classes

* fix(hooks): preserve file watching across errors and cwd changes

* fix(ci): isolate reference archive extraction per run

* test(ci): keep reference archive checks dependency-free

* fix(hooks): constrain recursive watches and repair CI

* test(web-shell): synchronize verified connection return

* fix(ci): correct runtime broker callback indentation

* test(web-shell): match the managed workspace registration label

Keep the remote workspace registration smoke checks in sync with the service label added by unified workspace navigation. Preserve exact text matching and all registration and replay assertions.

* fix(ci): reduce standalone runtime fixture overhead

* fix(hooks): ignore implicit atomic-write temporary files

* test(managed-agent): recover initial node port conflicts

* fix(hooks): observe parents before discovery anchors disappear

* test(managed-agent): await stream cleanup before recovery teardown

* test(managed-agent): align hosted shell fixture capabilities

* test(managed-agent): let OS choose initial harness port

* fix(ci): restore runner-local Java cache permissions

* test(runtime-broker): use native execution profile paths

* test(managed-agent): bind valid workspace upload probes

* test(managed-agent): reuse the upload probe digest fixture

* test(managed-agent): bind hook fault listeners to available ports

* test(managed-agent): align the native profile path fixture

* test(managed-agent): Wait for projection refusal retries

* ci: Prepare daemon Java cache before Node preflight

* test(managed-agent): check channel model calls after delivery

* fix(hooks): observe explicitly registered symlink targets

* test(managed-agent): separate consumption from relay snapshots

* test(managed-agent): target tamper injection by resource key

* test(managed-agent): exhaust attestation EOF retry in fault gates

* fix(ci): bound RocketMQ release downloads

* test(tui): await the restored assistant before capturing history

* fix(ci): use a checksum-pinned RocketMQ mirror

* fix(ci): provision missing browser test fonts

* fix(ci): quote the font probe environment

* fix(ci): prepare browser fonts without elevation

* test(core): compare complete prune reports without ordering

* fix(ci): repair only runner-local Java tool caches

* test(managed-agent): qualify fixture JDBC templates

* fix: isolate reconnect hooks and cancellation test fixtures

Skip chat initialization so MCP reconnect cannot re-enable file hooks, and keep cancellation fixtures isolated from the background worktree scanner.

* fix(ci): tolerate a missing Java cache helper on resumed releases

Resuming an existing release checks out a tag that may predate the helper, which failed all five Java jobs before setup-java ran.

* fix: finish the main merge for harness deadlines and workflow size

Use the renamed settle-timeout accessor in the merged hosted-harness tests,
and record sdk-java.yml's new size after the resumed-release cache guard.

* style(managed-agent): restore import grouping in merged verification tests

* test(managed-agent): require consumed child result mirrors

* test(web-shell): center repository settings capture

---------

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: yungsen.hys <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-11-06 21:43:01 +08:00
chencheng (云谦) 1b1f4455d9 ci: build the PR tree once and shorten the required path (#16450)
* ci: build the PR tree once and shorten the required path

The no-AK integration gate, the four web-shell smoke shards and Lint & Static each rebuilt the tree test_build already uploads, and the build cache rarely helped: most runs logged "29 of 29 workspaces changed" because the arch-keyed cache was split across the mixed x64/ARM64 pool and a prefix restore returned the newest entry instead of the base commit's.

- test_build restores the build its base commit saved (ci-build-cache.mjs key --at), drops runner.arch from the key, and runs the serve fast-path closure and core subpath checks against the built tree.
- The no-AK integration gate unpacks that tree and runs as three shards; integration_no_ak_result keeps the required check name.
- The web-shell smoke unpacks the same tree instead of building; consumers recopy the opentui native assets for their host (host-assets).
- The standalone runtime regression moves from unit shard 1 to shard 4.
- A java_only profile skips the TypeScript lanes for changes confined to the Java packages; Lint & Static runs the scripts/tests suites that name them.
- Drafts skip the integration gate and the browser smoke; ready_for_review starts the full run.

* ci: check the settings schema in test_build, which has the core dist

Lint & Static only had core's dist because the serve fast-path check built
it; with that check moved, Generate settings schema failed to import it.

* ci: keep the standalone regression on unit shard 1

Moving it did not shorten shard 1: the regression runs in the side lane,
in parallel with the CLI and core suites, and shard 1's time is its CLI
slice (576 s against 147 s on shard 4 for the same 181 files). That
imbalance is left to duration-based sharding.
2026-11-06 03:30:11 -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
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
Shaojin WenandQwen-Coder 36c8b34222 perf(core): load the MCP client's Google auth providers on demand (#16242)
* perf(core): load the MCP client's Google auth providers on demand

mcp-client imported GoogleCredentialProvider and
ServiceAccountImpersonationProvider statically, so every process that
loads the MCP client, including each resident Managed Runtime worker,
also loaded google-auth-library whether or not any server uses Google
authentication. createTransport is already async; import each provider
only for a server configured with it.

Add transport tests for the service-account impersonation provider,
which had none in this file. On Linux x86_64 an idle worker loads
0.24 MiB less JavaScript and holds 0.9 MiB less private memory.

* fix(core): ask for a restart when a Google auth provider chunk is missing

Now that the MCP client imports its Google auth providers on demand, the
bundle puts them in separate chunks. A background update that replaces
dist under a long-lived process made the next connection fail with
"Cannot find module", wrapped as a connection error for the server.

Move the restart guidance that contentGenerator already gives for its lazy
provider chunks into utils/errors (getModuleNotFoundError,
wrapLazyModuleLoadError) and apply it to both provider imports. The
original error stays as the cause.

Add google-auth-library to the ACP static closure guard, so a static
import of either provider fails check-serve-fast-path-bundle again.

* test(core): isolate the lazy Google auth chunk tests in their own file

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-05 18:08:58 +00:00
Shaojin Wen 5d45b42940 feat(verify-pr): give the sandboxed round the PR's review threads (#16300)
* feat(verify-pr): give the sandboxed round the PR's review threads

The sandboxed /verify round had no token and no review context. Its only
inputs were the metadata snapshot and, on a follow-up round, the previous
report. On #13652, Critical R1-26 was confirmed and then marked fixed
three days before a round that published merge-ready without mentioning
it. Eight hours later the review bot, at the head that round had
certified, still could not rule out its residual.

The verify job's trusted resolve step now writes review-threads.json
beside pr.json. It holds every review thread, resolved ones included,
since resolving is the author's claim, together with each reviewer's
standing CHANGES_REQUESTED review. It comes from one paginated GraphQL
query reshaped with jq. The snapshot is best-effort: on failure no file
is written and the step warns.

verify-pr-prepare.mjs sends the same query and writes the same shape for
the local path. It removes a previous copy first, so a failed fetch
cannot leave a stale file. A test runs the workflow's block verbatim and
prepare's transform over the same pages and requires equal output.

The skill adds report item 4, Existing review findings. Each term the
verdict turns on is computed from snapshot fields:

- a thread is blocking when its first comment is tagged Critical,
  Blocker, P0 or P1 by an owner, member or collaborator;
- a blocking thread is open when it is unresolved, or when that comment
  belongs to a standing change request, resolved or not. That is the rule
  GitHub's review decision applies.

An open blocking thread left unmeasured rules out merge-ready, and with
nothing failed the verdict is inconclusive. Findings at fail 0 would
publish as "passed with non-blocking findings". Other blocking threads are
re-measured as budget allows, and the verdict line counts those left
unmeasured. Change request bodies are listed but do not hold the verdict,
because their severities are prose. The duty is also a line in the scope
budget. A missing file reads as unknown, never as "nothing open".

The file adds 2396 bytes, so the size baseline records the exact 378242.

* test(verify-pr): pin the review snapshot's jq guard, copy order and odd answers

- The CI/local parity test runs the step's real jq program. Like the
  file's four other jq cases, it now skips when jq is missing, instead of
  failing one assertion later with a message that names neither jq nor
  the missing tool.
- The local recipe must clear the cached review-threads.json before it
  copies prepare's file. The test pinned the three lines but not their
  order. With rm after cp, every local round would read the review state
  as unknown.
- prepare's two other failure arms are now driven through the CLI: an
  answer that is not JSON, and pages without a pull request. Each must
  leave no file and say why. An empty state there would read as "nothing
  open".

* test(verify-pr): drive the review snapshot through the shared gh harness

The review-snapshot case had its own copy of the executed-step harness:
a temp dir, a stub gh on PATH and GitHub's bash flags. makeGhHarness
exists so that fidelity contract lives in one place. The factory now
records every call's argv, returns its temp dir, and has an
empty-stdout arm for paginated reads, GH_STUB_EMPTY_LIST. The snapshot
case runs through it. Its failure arms are the factory's
GH_STUB_FAIL_LIST and GH_STUB_EMPTY_LIST.

Deleting the shared stub's fail arm, its empty-stdout arm or its argv
capture turns the case red, and so does recording only the first
argument.
2026-11-05 23:50:44 +08:00
Shaojin WenandQwen-Coder edb094fea3 test(managed-agent): gate lifecycle-v2 close with a stopped worker (#16123)
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-05 22:22:26 +08:00
liuxuely0626andluoyun 0cc37bf444 feat: add Qwen subscription support across CLI, VS Code, and Desktop (#16378)
* feat(subscription): add development login, models and web search

* feat(subscription): refine account flows and host integration

Align desktop and VS Code subscription UI, add account logout and confirmed plan display, and map subscription thinking parameters. Remove simulation code and obsolete native authentication paths, consolidate design documentation, and cover cancellation and recovery boundaries.

* fix(subscription): preserve request errors and remove unused compatibility

* fix(subscription): refine command visibility and client logout flows

* fix(subscription): simplify account actions and desktop sign-in feedback

* docs(subscription): clarify sign-in and provider guidance

* feat(subscription): support switching accounts across clients

* fix(subscription): align account actions and open plans on demand

* fix(subscription): restore model selection after logout

Filter subscription-only commands consistently across terminal renderers, preserve model catalog error classification, and display the subscription authentication name consistently.

* fix(subscription): unify status authentication labels across clients

* fix(subscription): preserve provider connections across client logout

Restore configured providers after subscription logout across CLI, Desktop, and VS Code. Keep unrelated connections and conversation history, synchronize model state silently, and return to an unconfigured chat when no provider remains.

* fix(subscription): correct native client logout state

* feat(subscription): enable sign-in without development mode

* refactor(ui): 优化会员登录成功提示和界面交互体验

- 移除订阅欢迎提示相关状态和行为,简化 UI 状态管理
- 替换登录成功提示为四行摘要,包含邮箱、套餐、模型及常用命令提示
- 更新国际化文本,支持新的登录成功摘要内容
- 取消订阅模式下输入框下方的命令提示行,改为在登录成功摘要中展示
- 移除多余的测试用例,新增登录成功消息的单元测试校验
- 优化 AppHeader 组件,不再显示订阅启动重复提示
- 调整 Composer 组件逻辑,删除订阅提示相关副作用及渲染条件
- 修改历史消息显示组件以适应新的成功提示格式

* fix(subscription): enable Flash reasoning controls in shared UI

* fix(subscription): prioritize authorized models and simplify login dialogs

* fix(subscription): preserve gateway errors and request IDs

* fix(subscription): synchronize account state across clients

Preserve model selections after sign-out, synchronize account commands and model catalogs, and retain explicit browser sign-in flows across CLI, Web Shell, and VS Code. Cancel subscription background agents on logout and normalize expired refresh credentials without SDK error wrapping.

* fix(subscription): reset default model on explicit sign-in

Choose the default served model when explicitly signing in to Qwen Subscription, preferring Max over a previously saved Flash selection. Preserve normal startup behavior and other provider flows, and cover CLI and workspace activation with regression tests.

* fix(vscode): refresh models after successful sign-in

* fix(web-shell): hide account actions during automatic sign-in

* fix(web-shell): align model details and auth keyboard navigation

* docs(subscription): revert previous guide changes

* fix(core): reuse subscription catalog for same-login model switches

* docs(subscription): document Qwen membership sign-in

* fix(subscription): unify quota notices and preserve provider switching

Carry structured subscription quota details through chat and search errors for localized CLI, Desktop, and VS Code display. Keep other providers available when subscription discovery fails and refine sidebar account actions.

* fix(web-shell): align Chinese subscription entry title with CLI

* fix(subscription): open plans for saved non-member accounts

* fix(web-shell): show subscription commands after desktop sign-in

* fix(subscription): reauthorize explicit account sign-ins

* fix(auth): add model switching hint to subscription success

* docs(auth): list Qwen OAuth as the first option

* docs(models): simplify subscription sign-in guidance

* docs(models): remove legacy OAuth from supported auth types

* docs(subscription): link membership references and remove obsolete allowance note

* fix(subscription): preserve session until reauthorization succeeds

* fix(subscription): use pre for browser authorization and plans

* fix(subscription): use production endpoints

* test(vscode): fix editor message test formatting

* fix(subscription): preserve unauthenticated session startup

* fix(cli): classify subscription auth during ACP preflight

* fix(subscription): scope activation to the current project

---------

Co-authored-by: luoyun <[email protected]>
2026-11-05 20:44:21 +08:00
qqqys 08f1843c22 feat(mods): run the prompts a Mod submits in the Ink terminal (#16373)
* feat(mods): run the prompts a Mod submits in the Ink terminal

* test(mods): run a Mod's prompt in a real Ink terminal

* fix(mods): stop a Mod's prompts when the person stops a turn in Ink

- Esc or declining every tool a turn asks to run drops the Mods' queued
  prompts and refuses new ones until the person sends a prompt again. The
  stopped turn's turn.complete hooks run after the stop, and a call a hook
  does not await reaches the host after the hook returns, so dropping the
  queue alone let such a hook start the next turn. $.turn.abort still keeps
  the queue.
- A Monitor update waiting out its minimum interval no longer holds a Mod's
  prompt, which a Monitor printing within every interval held for good.
- A prompt put back after losing the idle moment runs once the winning
  submission settles, since that one may start no turn.
- The dual-output sidecar gets the Mod's text and a mod_prompt system
  message, as a stream-json client does, instead of the frame.
- The guide's capability table lists Ink for a Mod's turn events.

* fix(mods): drain Mod prompts after any settle and strip a stopped one

A Mod prompt queued while a submission that starts no turn held the
session (a cron's Mod command, a notification dropped before sending)
stayed queued with the session idle: the drain re-ran on settle only for
a prompt it had put back. Re-run it whenever a Mod prompt waits.

When the person stops a Mod prompt's turn before it answers, the framed
prompt stayed at the end of the model history, so the next prompt reached
the model behind it. The cancel info now says the turn ran a Mod's
prompt, and the handler strips it as it does a Goal continuation's.

* fix(mods): run a waiting Mod prompt once a Goal lets go of the turns

A Mod prompt queued while a Goal owned the session's turns waited for the
next submission to settle or another prompt to be queued. The Goal often
releases its turns after the session is already idle (its verifier accepts,
its last turn is released), so the prompt could sit until some unrelated
event. The drain now also re-runs on the Goal runtime's broadcasts while a
Mod prompt waits.

The test no longer queues a second prompt to wake the drain; it changes the
Goal snapshot and broadcasts, and fails without the fix.
2026-11-05 20:39:00 +08:00
Jiankai XiaoandJiankai Xiao de6ccb4e3c feat(serve): allow configuring the Conversations working-directory root (#16105)
* feat(serve): allow configuring the Conversations working-directory root

* test(serve): register Conversations root E2E in no-AK CI

* fix(serve): address Conversations root review feedback

* fix(serve): clarify Conversations root configuration errors

---------

Co-authored-by: Jiankai Xiao <[email protected]>
2026-11-05 16:53:05 +08:00
qqqys a818643533 feat(mods): let a Mod submit a prompt of its own in headless runs (#16302)
* feat(mods): let a Mod submit a prompt of its own in headless runs

$.prompt.submit({ text, asUser? }) raises prompt.submit through every hook
but the caller's, with the Mod as its origin, and queues what arrives
beneath them. The headless run takes it once idle and runs it as a turn of
its own: UserPromptSubmit hears it, turn.start and turn.complete fire, and
the transcript records it with the provenance mod_prompt and the Mod's
name. Unless the Mod passes asUser, the model reads the text inside a block
that names the Mod and says the person did not type it.

The call resolves once its prompt is queued, as next(e) does for a typed
prompt. The Mods' prompts start at most 8 turns before the person sends a
prompt. The terminals do not run these prompts yet: there the method is not
listed and a call resolves to a drop.

* fix(mods): queue a Mod's prompt once, outside its caller's lifetime

- Run the hooks a `$.prompt.submit` raises outside the calling hook, so a
  call the hook does not await still queues after the hook returns.
- Queue what last arrived beneath the hooks once they have all answered,
  so a hook that calls `next(e)` twice queues one prompt.
- Drop an empty or whitespace-only text, from the call or from a hook.
- Discard Mod prompts that have not started when a headless run ends, so a
  stream-json session does not run them on the client's next message.
- Escape every `<` and `>` in the framed text.
- Recall memory for the Mod's text rather than the frame, keep a Mod's
  prompt out of the session title (in its turn's place) and out of the
  commit attribution's prompt count.

* fix(mods): keep the title, SDK output and turn count right for Mod prompts

- Mark a Mod's prompt as `null` in the title projection, not `''`, so it
  no longer switches a session whose prompts recorded no display text to
  an empty projection, which stopped auto-title and `/rename --auto`.
- Tell a stream-json client of a Mod's prompt: its text as a user message,
  then a `mod_prompt` system message naming the Mod, before the turn runs.
- Reset the Mods' turn count for the person's slash commands too.
- End a Mod turn that a duplicate provider call stops as an error on the
  shared loop, as on the legacy loop and for the main turn.
- Refuse a prompt submitted while /clear or /resume ends the session, and
  start the new session with nothing queued and the count at 0.

* fix(mods): keep charging a hook while the other Mods hear its submit

A $.prompt.submit call outlives its caller, yet it lent the caller's
budget for the whole submit chain, so a hook that did not await it ran
unbudgeted until the other Mods' prompt.submit hooks answered. It now
runs as the void notices do, charging the caller as it goes.

The frame also no longer says the Mod sent its prompt while the session
was idle: most are sent mid-turn and wait for the session to be idle.

* fix(mods): run a Mod command's submitted prompt and keep it out of the title

A Mod command in a headless run always answers with a message, and that
branch returned before the drain, so a prompt the command submitted with
$.prompt.submit was discarded while the call had resolved as queued. The
reply is now shown without a turn of its own and the run goes on to the
drain, ending with one result.

A session whose prompts recorded no display text drew its title from the
conversation, which held the Mod's framed prompt. The recorder now keeps
the text the model read for each Mod prompt, and the conversation-based
title leaves those messages out.

* fix(mods): keep a Mod's prompt within its payload and its command's reply in view

- Drop a submitted prompt past half a Mod payload, from the call or a
  hook's rewrite, before it is queued, so a queued prompt is never
  reported as failed; an input too large for the bridge now rejects.
- Print a Mod command's reply in text output, and give the worktree notice
  the command would have carried to the first request the run sends.
- Leave an `asUser` Mod prompt out of the title keys, which matched a
  prompt the person typed alike, and keep the keys least recently used.
- Make Goal evidence leave a Mod's prompt out explicitly.
- Pin the documented turn limit and the tool-result guard in tests, and
  add a built-CLI test of `$.prompt.submit` against the fake server.
- Docs: six steps, the non-goal narrowed, the hook docs and the budget
  paragraph naming Mod prompts.

* test(mods): run the $.prompt.submit integration test in the no-AK job
2026-11-05 14:42:41 +08:00
Shaojin Wen ef9c891b8f fix(ci): clear the pinned corepack cache before every install (#16351)
* fix(ci): clear the pinned corepack cache before every install

The Linux test shards have been dying seconds into Install dependencies
with MODULE_NOT_FOUND on the cached pnpm's bin/pnpm.mjs since #16328
pinned COREPACK_HOME to runner.temp (main runs 41876940141, 41889761157,
41927887452; six different self-hosted hosts). The pin assumed runner.temp
is per-run, but self-hosted runners keep _work/_temp across jobs — the
lint job's isolated-HOME step already documents this. corepack trusts an
existing cache entry without re-verifying its files, so a partial pnpm
tree left by an earlier job fails every later job on that host.

Clear the pinned cache at the start of the shared install body in all
three jobs that copy it, enforcing the per-run contract the pin
documents. The guard keeps the reset a no-op where COREPACK_HOME is
unset (test_build) or already run-unique (lint). The test step still
reuses the cache install warms in the same job, and poisoned hosts
self-heal on their next job without manual cleanup.

Pin the reset and the install/test COREPACK_HOME match in the workflow
meta-test, and bump the size baseline for the added lines.

* fix(ci): scope the corepack cache reset to the shard-owned path

Review found two over-deletion shapes in the nonempty guard: a runner
that exports COREPACK_HOME would lose its entire shared cache in the
build job, and hosted lint would lose the cache its pnpm-store step had
just warmed, re-exposing the install to a registry outage. Match only
the exact shard-owned path instead; the build and lint copies become
unconditional no-ops and the three install bodies stay byte-identical.

Add an execution test that runs the shipped guard fragment under a
scrubbed environment across five cache shapes: it kills the dead-guard
mutant the string pins survived, and a widened-guard mutant dies on the
shared-cache survival cell. State the incident as observations with the
corruption source undetermined, and correct the stale per-run rationale
beside the pin.
2026-11-05 09:27:53 +08:00
Shaojin Wen 8a7c0d17d8 fix(managed-agent): find the Flyway scripts in the module jar the server ships them in (#16175)
* fix(managed-agent): find the Flyway scripts in the module jar the server ships them in

Since #15871 split the server into Maven modules, its Flyway scripts ship
in BOOT-INF/lib/qwen-managed-agent-persistence-*.jar rather than in
BOOT-INF/classes/db/. The upgrade command's check read only the classes,
so on every archive since then its database row warned "cannot compare the
Flyway scripts (no Flyway script found in ...)", and a release that changed
or reordered a script the installed database ran went unflagged.

migrationScripts() now also reads db/**/ at the root of each nested jar in
BOOT-INF/lib/. Spring Boot stores those uncompressed, so their directories
are read in place; a compressed one, which Spring Boot would not load, is
skipped. A script keeps its path as its name wherever it ships, so one moved
from the classes into a module compares as unchanged.

The reference distribution workflow now reads the scripts out of the exact
archive it built, so a later change of layout fails there rather than
turning check's row into a standing WARN.

* fix(managed-agent): read a Flyway script shipped twice with different contents as unreadable

The reader keys the scripts on their classpath path, and the nested jars
give that key a second source. Two copies of one path with different
contents left whichever the central directory listed last in the map, so
the database row compared one arbitrary copy, while the class loader would
hand Flyway the other. Matching copies are still one script; differing
ones now make the jar unreadable, which the row reports as "cannot compare
the Flyway scripts".

* fix(managed-agent): name both places that ship a differing Flyway script, and pin the local extra field

- When one script path ships twice with different contents, the error now
  names the two places, BOOT-INF/classes or the nested jar, so the
  operator need not unpack the jar to find the second copy.
- The test zips can carry a local-header extra field, as archivers add for
  timestamps or alignment, and the nested-jar case reads entries past one,
  so dropping that term from dataStart fails the suite.
2026-11-05 08:10:28 +08:00
Shaojin Wen b9e20830f7 fix(verify-pr): post flaky demotions on closed PRs and pin the blank class (#16135)
* fix(verify-pr): post flaky demotions on closed PRs and pin the blank class

The closed-PR skip from #16050 also caught the flaky variants of the
cancelled, failed-before-report and results-unavailable notices, and the
report body of a round that wrote no report.md. Those render ❌ and, on
a merged PR, name non-deterministic tests now on main, so they post
unless this run's own report already carries the demotion.

- publisher: the closed-PR skip leaves FLAKE_VERDICT=flaky alone,
  except beside this run's own report
- publisher: the gates line says its tests are "not counted
  individually above", since each suite run now counts as one
  assertion (zh: 不逐条计入上面的断言)
- checker tests: pin the blank class on their own (whitespace refused,
  U+0085 and U+FEFF accepted); before, only the shared parity table in
  the workflow suite caught a revert to trim()
- SKILL: rewrap the over-long local-path line

* fix(verify-pr): scope re-run advice to open PRs and pin the gate condition

The results-unavailable notice can now land on a merged PR, where the
verify lane refuses `@qwen-code /verify`: both variants now offer the
re-run only while the PR is open.

The flaky exemption keys on the gate verdict, which holds only because
the gate step runs on a prepared round; a test now pins that condition.

* fix(verify-pr): skip a flaky notice only beside a report that carries it

On a closed PR, a flaky notice with no live status of its own to
finalize is skipped beside this run's own report, as a duplicate of the
demotion that report carries. That report can come from an earlier
attempt whose gate did not diverge. A re-run attempt whose own live
status is missing (that post is best-effort) then lost its ❌ to the
run log, beside a report that never showed it.

A flaky verdict now adds a verify-flaky marker to the header, and the
closed-PR skip drops a flaky notice only when this run's own report
carries that marker in its header.

* test(verify-pr): pin the own-comment lookup's initializers

The publish step runs under `set -u`, and a failed comment listing or bot
identity lookup leaves OWN_ID, OWN_SUBSTANTIVE and OWN_FLAKY at their
initial values. Dropping any of those three lines kept the suite green:
no case made either lookup fail. On such a run the step would instead abort
with "unbound variable" and post nothing.

The upsert stub can now fail the listing or the identity lookup. Under
either failure, a report posts fresh rather than patching a status it cannot
see. A flaky notice on a closed PR also posts, because the lookup cannot
tell whether this run's report already carries the demotion. Deleting any
one initializer now fails the test on its own unbound variable, which the
harness now prints with the failed exit status.
2026-11-05 02:42:14 +08:00
Shaojin WenandQwen-Coder 9123d5ee04 fix(managed-agent): refuse every URL fetch refuses and say why a health probe failed (#16110)
* fix(managed-agent): refuse every URL fetch refuses and say why a health probe failed

start and verify refused a listener URL only when URL.canParse rejected
it, so a --server-url without a scheme (proxy.test:8443), with a user
name or password, or on a port the Fetch standard blocks (6000, 10080)
passed the check and then failed every probe until --timeout. They now
refuse each of those before anything runs, and a test checks that the
runtime's fetch rejects every URL the check refuses.

A probe that is sent and still fails now says what it got: start's
timeout names the last probe's answer and verify's health row names its
own, such as "HTTP 503, status DOWN", "self-signed certificate" or
ECONNREFUSED, where both said only "not UP".

The listeners() comment names the reachesHarness option verify passes.

* fix(managed-agent): refuse a Harness URL that is not http or https in upgrade start

start checked the Harness URL with new URL() alone, so an opaque value
such as proxy.test:8443 parsed with no host and no port: start then
waited out --timeout for the Harness to listen on port 80 of an empty
host. It now refuses any Harness URL that is not http or https before
it starts a unit, as the preflight would fail it later anyway.

The probe-answer test also runs the runtime's own fetch against a port
nothing listens on, so the ECONNREFUSED wording comes from a real error
rather than a constructed one.

* fix(managed-agent): keep the endpoint's answer when the deadline cuts start's last probe short

start's last health probe gets the 1 ms the deadline leaves it, so against a
real endpoint it times out before any answer arrives, and the timeout message
reported that abort instead of what the endpoint had been answering, such as
HTTP 503, status DOWN. A probe that times out on a budget the deadline cut
below the per-probe cap no longer replaces an earlier answer; one that runs
out of its own 5 seconds still does, since the endpoint stopped answering.

The test rig's fake fetch now awaits a response function, so a test can model
an endpoint that answers late and honours the abort signal. Also pins the
IPv6-zone diagnosis to a percent inside the brackets.

* fix(managed-agent): treat a health body the deadline stalls as a cut-short probe too

A health endpoint that sends its headers and then stalls the body made
response.json() reject with the signal's TimeoutError. That branch did not
report timedOut, so start's guard took the probe for an answer, and a probe
the deadline cut short replaced an earlier HTTP 503, status DOWN with
"HTTP 200, The operation was aborted due to timeout". The body-read branch
now reports timedOut as well.

The last-answer test gains two real-socket cases: a body that stalls after
its headers keeps the earlier answer, and a refused connection after an
answer replaces it, which pins the timedOut half of the guard. The design
doc says when the 5-second limit counts as the probe's own, and that any
other failure replaces an earlier answer.

* fix(managed-agent): sanitize health text and align upgrade refusals

Address review R1-1..R1-5 on the upgrade URL/health change:
- R1-1: neutralize terminal control bytes (C0/DEL/C1 -> U+FFFD) in the
  endpoint-derived health answer before it is interpolated into start's
  timeout refusal and verify's health row, so a queryable endpoint cannot
  move the cursor, split a row, or write the clipboard (OSC-52).
- R1-2: the Harness URL refusal reuses fetchRefusal when the value does not
  parse, naming an IPv6 zone exactly as --server-url does.
- R1-3: start's timeout labels the retained value 'the last answer', matching
  the design doc, since a deadline-cut probe keeps an earlier answer.
- R1-4: pin verify's URL refusal (listeners runs it with reachesHarness:false)
  with a test asserting it refuses before any probe.
- R1-5: tear down the 'closes' test server via t.after so a failing assertion
  cannot hang node --test to the job ceiling.

Tests: 63 pass; R1-1/R1-2/R1-4 mutation-witnessed (printable no-op, refusal
gated on reachesHarness, zone reason reverted each redden their pin).
Prettier and ESLint clean.

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

* test(managed-agent): exercise the health sanitization over a real socket

Add a real-scenario pin for R1-1: a local HTTP server serves a status field
carrying cursor moves, a newline and an OSC-52 clipboard write, and verify
runs against it through the runtime's own fetch — the real
health() -> printable() -> row() -> out() path, not the rig's fake. Asserts
the printed health row keeps no control byte while the readable text survives.
64 tests pass.

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

* fix(managed-agent): mask a refused URL's password and name why verify's API reads failed

Found on a real systemd deployment (real control plane, Harness, MySQL,
TLS and nginx):

- A refused --server-url was printed as typed, so a URL with a password,
  which fetch refuses, put that password in the output. The refusal now
  prints it as ***.
- verify's session and turn rows still said only "fetch failed" where the
  health row above them named the cause, such as "self-signed certificate
  in certificate chain". API reads now take the same cause.
- Text the API hands back (an error reason, a body that fails to parse,
  a Session ID, an event type) reached the session and turn rows with
  its control bytes; it is now neutralized as the health answer is.

The design doc says the probe answer's control characters show as U+FFFD.

* fix(managed-agent): show a health status that is no string as its JSON

printable() ran String() on what the endpoint returned, and String() throws
on a valid JSON object whose own toString is no function, such as
{"status":{"toString":1}}. health() is not inside a try, so on a real
deployment verify exited with "Cannot convert object to primitive value"
before it printed its health row, and start's probe loop stopped at the
first probe. A value that is no string is now shown as its JSON, which is
also what the endpoint actually said; an API error reason gets the same.

* fix(managed-agent): make the upgrade reports total and bounded, and refuse only what a phase dials

Review round 5 traced its findings to two shared causes, fixed at the
source rather than one site at a time.

Endpoint text in the reports (printable, withoutCredentials):
- printable() no longer throws on any parsed JSON value: a status nested
  deeper than JSON.stringify() recurses showed a bare "Maximum call stack
  size exceeded" from verify and left start's server running without its
  guidance; it is now shown as [unprintable].
- It also stops at 200 code points, cut after the control bytes are gone
  and never inside a surrogate pair, so a long status cannot scroll the
  report away. The item count on the session row goes through it too.
- withoutCredentials() masks the whole user information, so a token in the
  user-name position is masked, and masks it textually when the URL does
  not parse. The Harness refusal masks its URL as well.

URL checks (listeners, the Harness guard):
- A phase is refused only over a URL it dials: start and verify probe the
  health endpoint, and only verify --tenant reads the API. An API on a port
  fetch refuses behind a separate management port no longer stops start.
- The Harness URL is checked as the preflight checks it, an http(s) origin
  without user information, a query or a fragment, so start refuses it
  before it restarts a running Harness rather than after.

* fix(managed-agent): take the Harness verdict from the preflight and check the API URL only when verify reads it

- verify decides whether its session row reads the API before it checks
  the listeners, so a signed deployment, or --actor without a trusted
  actor header, is no longer refused over an API URL it never dials.
- The configuration resolver now prints the preflight's own verdict on the
  Harness URL, read untrimmed as java.net.URI reads it, and start refuses
  on it before restarting the Harness: a host with an underscore, an empty
  user name or a padded value no longer restart the Harness only to fail
  the preflight.
- withoutCredentials masks everything up to the last @ of a value the
  parser rejects, so a password holding / ? or # stays hidden.
- printable counts code points in one pass and keeps only the head, so a
  huge endpoint answer costs no copy of itself.
- The request an API error names is bounded like the rest of the row.

* fix(managed-agent): print the Harness URL untrimmed, as the server parses it

A padded qwen.managed-agent.harness.base-url fails the preflight, and the
start refusal now shows the padding instead of a value that looks valid.

* fix(managed-agent): judge the Harness URL only where the server reads it

The resolver passed on the preflight's verdict on the Harness base URL
with no regard to the Harness's state, so start refused a value that a
disabled or pooled Harness leaves unread: the preflight skips its
harness row before reading it, and the server never dials it. The
verdict is now "skip" there. start refuses on the preflight's grounds
(user information, a query, a fragment, or anything java.net.URI reads
differently) only on a "false" verdict, and its own reasons only word
that verdict. It still refuses a URL that is not http or https, since it
waits for the Harness there. Without a verdict, as when the
configuration does not resolve, the preflight judges the URL once the
Harness is up.

verify now checks the type of the values the API chose before using
them. Building a route from a Session id, matching an event type, or
reading an event sequence coerced the value first, and that throws on
an object such as {"toString":1} or on an array nested deeper than the
stack, so printable() never got to show what came back. A Session
without a string id fails its row with that value, an event type that
is not a string fails the turn row, and a sequence counts only as a
number or a numeric string.

The start tests now check every run's output for the password, not
only the last run's.

* fix(managed-agent): judge a pooled Harness URL, and read only lists from the API

The previous commit gave "skip" for a pooled Harness as well as a
disabled one, since the preflight skips its harness row in both before
reading the base URL. A pooled server still reads that URL, though:
each member's QwenHostedHarnessConnector is built from it and refuses it
by the same rule as the preflight. The pool's refresh drops a member
whose connector refuses it, so such a pool accepts no Session while the
server reports UP. Only a disabled Harness gets "skip" now. A test pins
the verdict to whether the connector builds, for a single Harness and
for a pool. Since the verdict is now the server's rather than only the
preflight's, the refusal says "the server does not take".

verify reads Sessions, items and events only as lists. Data that is not
a list fails its row and says what came back: iterating it would throw,
or walk a string, and its length could be any value. A listed entry of
0, "", false or null fails the session row instead of being reported as
no Session. An event sequence moves the cursor only as a whole number,
and only forward: Number() reads "Infinity", "0x10" or " 4 " as numbers.

The user guide now says that, when the configuration does not resolve,
the preflight judges the Harness URL after the Harness starts.

* fix(managed-agent): keep start's own Harness URL rules without a verdict

When the configuration does not resolve there is no verdict. The previous
commits then let start accept any http(s) Harness URL and left the URL to
the preflight. A pooled preflight never reads it, though, and every pool
member's connector refuses user information, a query or a fragment, so
the pool would start and accept no Session. Without a verdict, start
refuses those itself again, since the server refuses them wherever it
reads the value. A "true" or "skip" verdict still asks nothing more, and
the generic wording is kept for a "false" verdict.

HarnessConfigurationTest now builds a pool through the real member
factory and shows that the single base URL decides whether a member is
accepted. The pooled half of the agreement test checked nothing the
single half did not, so it is gone.

verify now requires every page to carry its data as a list: a body that
is no object, or one whose data is null or missing, fails the row instead
of reading as no Session. A Session id of "", "." or ".." fails, since
fetch resolves dot segments and an empty id reads the list. Only
terminal: true ends a turn.

The docs no longer say the server takes only an origin, since a path is
fine. They say which component refuses the URL, and they word the
sequence rule as the code applies it.

* fix(managed-agent): sort the test's static imports, and refuse a --server-url query

The Mockito static import sat between two JUnit ones, so the verification
module's ImportOrder check failed. The static block is now sorted.

start and verify append /actuator/health and the API paths to
--server-url, so a query or a fragment there took the paths in. With
http://proxy.test/?a the command probed /?a/actuator/health, and fetch
dropped a fragment along with the path after it. Such a URL is now
refused up front.

A listed Session entry with no id is now shown as itself rather than as
"undefined". A health answer whose JSON parse error quotes control bytes
is pinned to come out sanitized.

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-05 01:42:38 +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
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
qqqys a85370b38f feat(cli): run the Ink TUI's model rounds on the shared loop's round (#16206)
* feat(cli): run the Ink TUI's model rounds on the shared loop's round

With experimental.unifiedAgentLoop on, every model round of the Ink TUI
goes through the shared loop's runModelRound, which collects the tool
calls, drops those of an attempt a retry or fallback replaced, and turns
the client's terminal events into a stop reason. The TUI keeps its
stream processor and its tool step. Adds the Ink parity suite.

Part of #15327.

* test(cli): tighten the Ink parity suite and fix the no-AK script pin

- add the suite to the no-AK script pin
- check each run's request count, and through the debug log that only the
  run with the setting on sends its rounds on the shared loop
- compare the system prompt, the tools offered and recorded usage
- run each scenario once, without the root config's retries, with a
  source pin
- describe the suite as it is in the design doc
- say in the setting's description that the Ink TUI sends its model
  requests through the shared loop
2026-11-04 07:36:48 +00:00
56fec7f3f5 ci: shard the Linux unit tests and web-shell smoke, and reuse cached builds (#16133)
* ci: shard the Linux unit-test lane eight ways behind one build

The Linux Test job ran 45-59 minutes: a ten-minute prepare build, then the CLI and core suites back to back. A new test_build job runs the prepare build once and uploads its outputs; eight shards install without the build, unpack them and run --shard=N/8 of every Vitest suite. Test (ubuntu-latest, Node 22.x) becomes an aggregate that passes only when the build and every shard pass.

* ci: record ci.yml's growth from the test_build prelude

test_build carries the same pre-checkout cleanup, checkout and install prelude as the test shards; ci-platform-lanes.test.js pins the two copies identical.

* ci: record the Linux lane's two new jobs in the PR-stage budget

test_build and the test_ubuntu aggregate add two jobs; the declared timeouts drop from 305 to 295 minutes.

* ci: shard the web-shell E2E smoke four ways

The smoke lane ran 44-46 minutes on every recent PR, about 33 of them in the browser tests at two workers, and is the longest lane once the Linux unit tests are sharded. Each shard runs --shard=N/4 of the smoke; the transcript gate runs on the first. web-shell E2E Smoke (ubuntu-latest, Node 22.x) becomes an aggregate over the shards.

* ci: bound the unit shards' tree download and retry it once

A pool host stalled 17 minutes in download-artifact while the other shards took under a minute.

* ci: rebuild only the workspaces a change reaches

scripts/ci-build-cache.mjs hashes each workspace's tracked build inputs (tests excluded) with its workspace dependencies' hashes and the shared root inputs. test_build and the web-shell smoke shards restore the newest cached build and rebuild only workspaces whose hash changed. A push to main always builds from scratch and saves, so a pull request starts from a clean build of main.

* docs(ci): describe the incremental build cache

* ci: keep the build cache path the same on every pool host

actions/cache versions an entry by its path; under runner.temp the path differs per host, so a run could never restore another host's save.

* ci: save the ARM64 build cache from the smoke lane, and run the aggregates on the shards' runners

The smoke shards run on the ARM64 pool, which test_build may not, so the first shard saves the clean build it made when its restore found nothing. The two aggregate checks queued 9 and 13 minutes for a hosted runner after their shards finished; they now take the shards' runners.

* ci: bound the sharded test step and skip shards when the build fails

Address the first automated review round on the Linux sharding lane:

- Gate every build-consuming shard step on `test_build` success. `!cancelled()`
  stays true when a needed job fails, so a failed build still started all eight
  shards, each paying a full install before dying at the missing artifact and
  reporting artifact plumbing as its failing step.
- Drop the test step cap to 45 minutes. The job cap is 60, so the old 110 could
  never fire; a step timeout keeps the diagnostic steps below it alive while a
  job timeout does not.
- Keep the built-tree artifact for a week so a late "re-run failed jobs" still
  finds it, and correct the trigger comment that still named three post-merge
  jobs when five now accept push.

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

* ci: keep disk telemetry for a failed build and correct the design doc

`test_build` installs with the disk sampler and then builds for up to its
whole timeout, but the sampler dies with the install step's trap and no
collector ran, so an ENOSPC build (or the OOM that took a shard host) kept
no evidence. Give it the same failure-gated dump and last-step upload as
`lint_and_static`, under its own artifact name.

The incremental-lint design still said the test job keeps normal prepare
behaviour; this lane now installs with `QWEN_SKIP_PREPARE=1` and unpacks
the tree built once by `test_build`. Correct both language versions.

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

* ci: share the built-tree definition and the build preflight, harden the guards

The archive step re-implemented `builtPaths()` from `scripts/ci-build-cache.mjs`,
so the tree the shards unpack had a second definition that could drift from the
one `pack` stores. A `paths` mode now prints that list for `tar -T -`, and a new
test pins the four artifact-name literals that tie the producer to the eight
consumers.

The cached branch of `build` also hand-reproduced a subset of `scripts/build.js`
and so skipped its ink-patch preflight: the same missing
`frame-controller.d.ts` self-healed on a cache miss and surfaced as ~50 tsc
errors inside ink's declarations on a hit. The preflight is now a function
reachable as `scripts/build.js --preflight` and runs before both branches.

The remaining review suggestions were all meta-test gaps: the standalone-runtime
expression and the `test_build`/`test` prelude were guarded by nothing that
compares them in order, `ci-build-cache.mjs`'s three CI modes were never
executed by a test, and the infra-classification filter's step name was not
joined to `ci.yml`. Each now has an assertion that goes red when the guard is
removed.

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

* ci: bound the build steps so the failure collectors can run

Round-2 review found four issues in the previous round's fixes and one gap
in the new build tests.

A job timeout cancels the job and skips the failure-gated collectors, so the
`test_build` steps that can block now carry caps below the job's 45 minutes:
the install, the build, the pack and the archive. The install caps are
identical across `test_build`, `test` and `lint_and_static` so the prelude
parity guard still compares the three copies field for field. A test pins
every blocking cap under the job ceiling.

The `--preflight` early exit was only asserted as a string, so it could be
renamed with nothing going red while the cache-hit path silently ran the
full build. A spawned case now copies `scripts/build.js` into the fixture
and asserts the flag exits without generate, a workspace build or the
schema step.

The CLI mode tests write extension-less `#!/bin/sh` stubs and join PATH with
':', which cmd.exe cannot resolve; the Windows lane collects that file and
downgrades its failure to a warning, so it would have carried a permanently
red block. The suite is excluded on win32 alongside the other bash-fixture
suites.

The `test_build`/`test` intersection grew to 16 with the disk collector, but
the floor stayed at 15, leaving exactly one shared step uncompared; the floor
now matches the membership. The cache-hit-with-changes branch — the code
that decides what is rebuilt and what is deleted — gains a case: a changed
workspace is purged and rebuilt by directory, and an untouched workspace
keeps what the cache restored.

ci.yml's recorded size baseline is updated to the current file so the next
PR gets the full growth allowance back.

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

* test(ci): keep cache fixture call logs outside the archived tree

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

---------

Co-authored-by: 易良 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-04 14:52:12 +08:00
qwen-code-dev-bot 4d5e30e6f0 fix(test): wait for the RocketMQ port listener to clear after stop (#16252)
Aligns the parked-port release assertion with the script's own listener check: on a shared pool host a fresh bind probe can fail for a foreign socket or race the killed listener's asynchronous socket release (release run 41708821484, Scripts lane). The sibling case already uses waitForNoListener.

Refs #16248
2026-11-04 14:30:26 +08: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