qwen-code/scripts
Shaojin Wen 358091833b
fix(ci): watchdog silent sandbox hangs and reap the containers they leak (#8816)
* feat(ci): A/B deterministic gate rejections against the pre-round ref

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: verify <verify@local>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci@service.alibaba.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-10 11:00:11 +00:00
..
installation fix(packaging): bundle clipboard addon in standalone builds (#6708) 2026-07-11 15:18:24 +00:00
lib feat(core): support QWEN_HOME env var to customize config directory (#2953) 2026-05-09 15:51:52 +08:00
tests fix(ci): watchdog silent sandbox hangs and reap the containers they leak (#8816) 2026-08-10 11:00:11 +00:00
acp-http-smoke.mjs feat(daemon): merge daemon-mode feature batch into main (#4490) 2026-06-12 00:34:49 +08:00
audit-runtime-critical.js ci: keep the critical-audit gate honest when npm cannot answer (#7743) 2026-07-26 06:59:13 +00:00
benchmark-api-latency.mjs feat(cli): add API preconnect to reduce first-call latency (#3318) 2026-04-27 06:54:55 +08:00
build-hosted-installation-assets.js fix(installer): auto-detect SYSTEM account and default PATH scope to machine (#4903) 2026-06-10 21:02:10 +08:00
build-standalone-release.js fix(packaging): bundle clipboard addon in standalone builds (#6708) 2026-07-11 15:18:24 +00:00
build.js feat(channels): GitHub polling adapter with notification-as-wakeup architecture (#7632) 2026-07-25 09:31:50 +00:00
build_package.js fix(build): clean stale outputs before tsc --build to prevent TS5055 (#4453) 2026-05-23 23:06:31 +08:00
build_sandbox.js fix(sandbox): fall back to 'latest' tag when image name has no colon (#2962) 2026-04-18 09:07:05 +08:00
build_vscode_companion.js Sync upstream Gemini-CLI v0.8.2 (#838) 2025-10-23 09:27:04 +08:00
check-build-status.js fix(review): report what the transcripts prove; build the roster in one call (#7033) 2026-07-18 00:43:57 +00:00
check-desktop-isolation.js feat(desktop): package Web Shell as a release-ready desktop app (#8132) 2026-08-02 08:20:16 +00:00
check-i18n.ts fix(cli): localize approval mode UI labels (#6592) 2026-07-11 00:07:03 +00:00
check-lockfile.js Sync upstream Gemini-CLI v0.8.2 (#838) 2025-10-23 09:27:04 +08:00
check-serve-fast-path-bundle.js feat(ci): fail the startup bundle check when the CLI entry is hoisted into a chunk (#8203) 2026-07-31 08:57:57 +00:00
check-voice-guard-sync.js feat(voice): support trusted private ASR base URLs (#8350) 2026-08-06 14:04:57 +00:00
clean-package-build-artifacts.js feat(channels): GitHub polling adapter with notification-as-wakeup architecture (#7632) 2026-07-25 09:31:50 +00:00
clean.js feat(desktop): Add desktop app package with Qwen ACP SDK integration (#3778) 2026-06-11 21:57:20 +08:00
cli-entry.js fix(cli): preserve Qwen Review startup version in footers (#8431) 2026-08-04 14:58:56 +00:00
copy_bundle_assets.js feat(review): say so when the bundle is older than the review it runs (#8390) 2026-08-07 03:21:26 +00:00
copy_files.js refactor(core): move review skill incident narratives to DESIGN.md (#8499) 2026-08-04 12:41:18 +00:00
create-standalone-package.js feat(review): say so when the bundle is older than the review it runs (#8390) 2026-08-07 03:21:26 +00:00
create_alias.sh fix: ambiguous literals (#461) 2025-08-27 15:23:21 +08:00
daemon-dev.js fix(scripts): allow multiple dev:daemon instances by probing Vite port (#7212) 2026-07-19 12:49:47 +00:00
desktop-openwork-sync.ts feat(acp): support desktop qwen integration (#4728) 2026-06-09 19:09:44 +08:00
dev.js fix(review): report what the transcripts prove; build the roster in one call (#7033) 2026-07-18 00:43:57 +00:00
esbuild-shims.js perf(cli): code-split lowlight to cut startup V8 parse cost (#4070) 2026-05-15 17:26:18 +08:00
generate-changelog.js feat(release): generate AI-assisted release notes (#6756) 2026-07-12 13:00:22 +00:00
generate-git-commit-info.js # 🚀 Sync Gemini CLI v0.2.1 - Major Feature Update (#483) 2025-09-01 14:48:55 +08:00
generate-release-notes.js fix(release): raise model timeouts and shrink batch size for slow networks (#8007) 2026-07-29 22:38:35 +00:00
generate-settings-schema.ts revert: remove local PR verification gate (#7031) 2026-07-16 11:24:38 +00:00
get-release-version.js fix(release): bump preview base past published stable (#7978) 2026-07-29 23:21:00 +00:00
lint.js ci: run Windows merge queue tests on ECS (#8386) 2026-08-05 12:14:42 +00:00
local_telemetry.js Merge tag 'v0.3.0' into chore/sync-gemini-cli-v0.3.0 2025-09-11 16:26:56 +08:00
measure-flicker.mjs fix(cli): bound SubAgent display by visual height to prevent flicker (#3721) 2026-04-29 22:34:55 +08:00
pre-commit.js Sync upstream Gemini-CLI v0.8.2 (#838) 2025-10-23 09:27:04 +08:00
prepare-package.js feat(core): add full-resolution image zoom tool (#7809) 2026-07-27 23:40:26 +00:00
prepare.js feat(web-shell): git status chip, visual working-tree diff, and sidebar git status (#7054) 2026-07-18 10:06:07 +00:00
release-script-utils.js feat(installer): add standalone hosted install and uninstall flow (#3828) 2026-05-21 11:57:10 +08:00
run-java-daemon-sdk-e2e.ts ci: reduce SDK Java runner queueing (#8441) 2026-08-03 16:21:05 +00:00
sandbox_command.js fix(scripts): avoid shell injection in sandbox command detection (#6108) 2026-07-01 16:20:40 +08:00
sdk-node-exporter-stub.js perf(telemetry): lazy-load the SDK and split OTLP exporter chains by protocol (#7276) 2026-07-21 07:35:30 +00:00
sign-release.sh feat(cli): add standalone auto-update support (#4629) 2026-06-04 22:53:12 +08:00
start.js fix(review): report what the transcripts prove; build the roster in one call (#7033) 2026-07-18 00:43:57 +00:00
sync-computer-use-schemas.ts feat(computer-use): configurable screenshot max dimension (setting + env) (#5122) 2026-06-15 15:25:27 +08:00
telemetry.js feat(core): support QWEN_HOME env var to customize config directory (#2953) 2026-05-09 15:51:52 +08:00
telemetry_gcp.js fix(mcp): update OAuth client names and improve MCP commands 2026-02-08 10:46:48 +08:00
telemetry_utils.js feat(core): support QWEN_HOME env var to customize config directory (#2953) 2026-05-09 15:51:52 +08:00
test-rewind-e2e.sh fix(test): update rewind E2E Test 1 assertion after isRealUserTurn fix (#3622) 2026-04-26 06:49:42 +08:00
test-windows-paths.js chore: consistently import node modules with prefix (#3013) 2025-08-25 20:11:27 +00:00
unused-keys-only-in-locales.json feat: add /diff command and git diff statistics utility (#3491) 2026-05-10 11:15:59 +08:00
upload-aliyun-oss-assets.js fix(release): move constants above entry point to avoid TDZ error (#4398) 2026-05-23 22:21:33 +08:00
verify-capture.mjs fix(ci): avoid verify capture color conflict (#8236) 2026-07-31 14:15:40 +00:00
verify-installation-release.js feat(installer): verify release assets + switch public docs to standalone entrypoint (#3855) 2026-06-04 17:23:04 +08:00
version.js fix(release): pin channel-base dep to exact version during release bump (#7953) 2026-07-28 16:58:28 +00:00
workspaces.js feat(desktop): Add desktop app package with Qwen ACP SDK integration (#3778) 2026-06-11 21:57:20 +08:00