Commit Graph
11957 Commits
Author SHA1 Message Date
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
Job NatdhanaiandClaude Opus 5.5 a18ecb26c8 docs: configure Qwen3.x served under a custom model name (#16342)
* docs: configure Qwen3.x served under a custom model name

The local self-hosted section never pointed at the reasoning override, so
a Qwen3.x model served as e.g. --served-model-name chat kept thinking on
in side requests and used the 200K default window. Add a short subsection
with a model entry that declares the qwen-chat-template profile and the
server's context window, and the server flags tool calling needs.

Refs #16340

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

* docs: move the custom model name subsection to the end of the local section

Keep the API key and note blocks under Local Self-Hosted Models, as
suggested in review.

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-05 06:44:24 +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
2800ea18da fix(cli): keep project hook approval env inside the workspace (#16316)
* fix(cli): keep project hook approval env inside the workspace

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

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

Fixes #16313

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

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

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

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

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

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

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

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

---------

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-05 02:10:58 +08:00
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
Shaojin WenandQwen-Coder 4d4e4fc9ca feat(channels): role-aware action templates for GitHub bound-session delivery (#16258)
* feat(channels): role-aware action templates for GitHub bound-session delivery

Bound-session delivery injected a hardcoded generic prompt and routed each event to a single winner, so a PR followed by review, verify, and fix sessions in different workspaces could only ever reach one of them, with no declarative way to say what each should do.

Bindings gain an optional closed-vocabulary role (review/verify/fix): the command-position backfill mints review/verify from /review and /verify-pr, fix binds explicitly through the metadata route (null clears), and a re-offer never overwrites an existing role. A new channel-level boundSessionActions record maps event classes (watch:<type> or notification reason, optionally role-qualified) to operator instruction templates rendered with GitLab-style %var% substitution into a labeled trusted block; substituted GitHub values stay sanitized, capped data.

With the map configured, routing generalizes the base design's single-winner decision to one winner per role with fan-out across roles: per-(event, target) durable tasks, independent backpressure, role-scoped gone fallback, and public fallback only at zero live followers. With the map absent, every behavior and the generic prompt stay byte-identical (pinned by tests).

Design: docs/design/2026-11-04-github-channel-bound-session-action-templates.md (EN/ZH); the base design's Key decision 4 carries a forward pointer.

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

* test(cli): expect the command-stamped role in backfill binding assertions

The /review command now stamps role: 'review' on every binding form it mints (role-aware bound delivery). Four strict-equality assertions in the backfill suite predated the field: the Aone URL-form and unplanned-repair cases, the fork-url state-pairing case, and the capped-plan live-bridge sync projection. CI's sharded Test lane surfaced only the first before fail-fast skipped the remaining shards; the full local suites (backfill 177, server 1442, bridge 1043, transport 431, daemon-worker 182, session-list/pr-refresh 87) now pass.

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

* fix(web-shell): delete a record key when its editor row is cleared

Addresses the triage review's record-renderer observation. The save serializer already filtered empty values (so no unsavable state existed), but the form state kept a touched-then-cleared key as "" until save; the editor now deletes the key on clear, matching what the upsert payload will contain. A new dialog-level test pins the end-to-end outcome for the 80-key boundSessionActions surface (and the GitLab record fields sharing the renderer). Also documents at roleByNumber that per-number uniqueness is guaranteed upstream by noteMention, so the Map constructor's last-wins semantics cannot override the first-wins command rule.

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

* fix(channels): close the role-aware delivery review findings (C1-C5, D1-D3)

C1: the first task of a multi-target comment dispatch now carries the full fan-out intent; recovery reconstructs missing siblings BEFORE applying the survivor's event-wide dedupe, so a crash between claims no longer loses the unclaimed roles permanently.
C2: a durable bounded delivery ring (github-bound-delivered.json) records every private acceptance; no gone-fallback or untargeted recovery may route an already-delivered event publicly, even after every follower disappears and the daemon restarts. An unreadable ring fails closed for the public fallback.
C3: delivery-time re-validation retires a role-aware comment whose role lost its match on a complete snapshot instead of injecting the obsolete role's instruction into the frozen session.
C4: split-sidecar merge lets the newer half win on role, absence included, so an explicit role:null clear survives an archive/restore merge.
C5: FIELD_LABEL_KEYS.github registers boundSessionActions, making the zh-CN label and description reachable.
D1: the map-skip retire now requires a complete fresh snapshot; under a partial or failing scan the event defers as backpressure instead of being consumed on incomplete evidence.
D2: comment-lane events resolve a lane class (comment) after the thread reason, so fix:comment catches comments delivered under mention/subscribed/author while reason-specific keys still win.
D3: the worker accessor validates roles against the shared PR_BINDING_ROLES constant instead of hardcoded literals.

Each correctness fix has a dedicated regression; C1/C2/C3 regressions were mutation-witnessed (removing the fix turns them red). Design docs (EN/ZH) and the user guide updated to the new semantics.

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

* fix(channels): close the role-aware delivery re-review findings (C1 residual, R2-1..R2-4)

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

* fix(channels): complete fanout slots durably and keep private retirement terminal (R2-2/R2-4 residuals)

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

* fix(channels): make the fanout completion mark fail-closed with durable acceptance evidence (R2-2 write-failure gap)

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-05 01:36:38 +08:00
jinyeandClaude Opus 5.5 b2463bf606 perf(managed-agent): share the storage fence in session command admissions (#16329)
MCP and extension management checks, model and approval changes,
context compression, task cancel and action responses checked the
storage fence with exclusive locks, so every Session on one storage
queued on its alias and directory rows. None of them writes those rows
later in its transaction, so they now use the read-only admission form
(FOR SHARE on MySQL, the exclusive fallback on H2). Fence writers still
wait for every admitted command. The publication writer stays exclusive
because its receipt lane may commit with lifecycle authority in the
same transaction.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-05 01:28:41 +08:00
jinyeandQwen-Coder b9f4a9ee43 fix(managed-agent): bound runtime thread pools (#16330)
Bound HTTP dispatch and local runtime operations, readers, and stop escalation. Preserve retryable admission refusal and durable recovery identity under saturation.

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-04 17:22:03 +00: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
Shaojin Wen 8e9b429ff8 perf(managed-agent): index the Runtime slot lookup by isolation key (#16325)
The CSI marker probe runs on every ordinary Session write, including each
activation renewal, and looks up the Session's Runtime slots by tenant and
isolation key. No index covered that lookup, so every call scanned the whole
slot table, and the fence's locking variant locked every Session's slots.

Index ma_runtime_slot.isolation_key, as ma_runtime_binding already does. A
MySQL integration test pins the probe's plans at 1,000 and 10,000 slots and
checks that the fence leaves other Sessions' slots writable while still
holding off a second slot for the fenced Session.
2026-11-05 00:51:22 +08:00
jinye 624d10532a perf(managed-agent): avoid storage locks in read preflight (#16312)
* perf(managed-agent): avoid storage locks in read preflight

* codex: fix CI failure on PR #16312
2026-11-05 00:13:09 +08:00
Shaojin Wen f217a8e5d9 fix(managed-agent): restore JournalCommit under its class-size ratchet (#16323)
#16284 pinned JournalCommit at 880 lines and #16275 grew it to 886, so every
Java lane fails ClassSizeRatchetTest on main.

The hosted-cleanup admission probe that #16275 added belongs with the cleanup
bookkeeping it guards, so move it behind
HostedCleanupReferences.containsCleanup and restore the class to its pin.

Fixes #16321
2026-11-04 23:54:05 +08:00
Shaojin Wen d4083f4c82 fix(serve): read PR sidecars directly for the bindings snapshot (#16241)
* fix(serve): read PR sidecars directly for the bindings snapshot

GET /sessions/pr-bindings paginated the activity-ordered session list
and had to flag every multi-page scan partial, so a workspace with more
than one page of active sessions never produced a complete snapshot and
the GitHub channel deferred every unmatched PR event to the dead-letter
horizon instead of answering through the channel.

List the workspace's *.pr.json sidecars once (the enumeration the PR
refresh timer already uses), read each sidecar, and take attribution
and activity from the session's persisted record, refined by the live
session when it is live. partial now means an I/O failure only. Scan
draining and transitioning workspaces too: their files stay readable
and they still serve sessions. Exclude machine-owned agent task
sessions explicitly. Update both design docs, the user guide and the
adapter's partial-snapshot diagnostics accordingly.

* test(serve): compare aggregated PR bindings as a set

Directory listing order and the random temp directory names made the
cross-workspace assertion order-dependent; CI sorted the uppercase
suffix first. Compare the (workspace, session) pairs as a set.

* fix(serve): harden the PR bindings sidecar scan

Append bindings the bridge holds in memory but that have not reached a
sidecar yet, so the window between the bridge update and the sidecar
write (or a failed write) cannot read as an authoritative absence.
Treat a transcript that exists but yields no records as unreadable
(partial) instead of listing the session unattributed, skip an active
sidecar left behind by an archived session, decide non-targets before
counting failures so a machine-owned session's corrupt sidecar cannot
make the answer partial, emit tombstone rows without the transcript
projection, validate live timestamps before the recency comparison, and
log per-session failures as one line per workspace scan.

Remove the write-only prsReadFailed flag now that the snapshot reads
sidecars itself, retire the last draining-workspace wording from the
adapter's diagnostics, and state the real per-row cost and the reason
for having no page cap in the design doc.

* fix(serve): treat a failed live session list as a partial bindings answer

The live list is the only source for a binding that has not reached a
sidecar, so a failed read must make the answer partial instead of
reporting it complete without those bindings. Merge bridge-held PR
numbers into a listed non-empty row (a tombstone stays empty), decide
hidden-catalog and archived ownership for empty sidecars too, compare
activity timestamps with the shared chronological rule, and log a
registry entry without a runtime like the other partial causes.

Reword the adapter's partial diagnostics to name every cause and count
listed rows, update the shared snapshot contract in channel-base, pin
the corrupt-sidecar row's presence in the session-list test, and make
the design docs' cost model consistent.

* fix(serve): decide live-only binding ownership from the persisted record

A restored agent task session carries no sourceType on its bridge entry,
so the live-only branch read ownership from a field that is not there
and emitted a machine-owned session as a delivery target in a complete
answer. Read the persisted record first, as the sidecar loop does, and
drop the isArchived guard the bridge never populates. Cap the bridge
merge the way the session list does (append only while the persisted
list is below SESSION_PR_LIST_LIMIT, then keep the newest) and log the
no-runtime cause under test.

Keep the adapter's partial hint firing while retention holds bindings
an empty partial no longer lists, and stop blaming the binding lookup
when the cold-empty cap was reached by successful empty partials. Name
the live-list cause in the design docs' partial definition and state
the two cost legs separately.

* fix(serve): keep unknown sessions unlisted when merging bridge bindings

A session whose sidecar or transcript could not be read is counted as
unknown and left out of the answer, but the bridge merge re-listed it
from its live PR subset, which made the adapter treat it as enumerated
and drop the row it was retaining with the persisted numbers. Track the
unknown sessions and skip them in the merge. Attribute live-only rows
through the same resolver as sidecar rows so an unreadable transcript
makes them unknown too, count live-only sessions in the diagnostic's
denominator, and pin that transitioning and blocked workspaces with a
runtime are scanned.

Make the adapter's partial hint say the lane is deferred while the
cold-empty gate holds instead of claiming issue events flow, and name
the binding lookup when failed lookups seeded the cold-empty cap and a
single empty partial lands it. State the live-merge rule and the real
filesystem-leg ceiling (sessions ever bound) in both design docs.

* fix(serve): decide binding ownership before any read that can fail

A machine-owned session whose transcript or sidecar could not be read
was counted as a scan failure before its ownership was consulted, which
made every answer partial on behalf of a session that is never a
delivery target. Resolve ownership from whichever record states it as
soon as the id is known, and skip non-targets before the sidecar read.
Key the bridge top-up by PR number and repository so same-numbered PRs
from different repositories both survive.

Log once per streak when the channel worker itself downgrades an answer
by dropping malformed rows, and name that cause in the shared contract,
the user guide and the adapter's hint. Make the failure-path cold-empty
cap message say the lane stays closed while no snapshot has ever
succeeded, and align the design docs' risk summary with the ever-bound
cost ceiling.

* fix(channels): attribute the cold-empty cap by counted failed lookups

Follow-ups to the PR bindings sidecar scan review (#16241, round 6).

- The GitHub channel's cold-empty cap line attributes the stall from the
  failed lookups counted within the streak. It used to read
  boundSnapshotFailureStreak, which any successful answer resets, so a run
  of failed lookups broken by a single empty partial was blamed on the
  daemon's storage. The majority cause leads and the other count is kept.
  The same line now says the worker reports how many rows it dropped
  rather than naming one.
- The bindings route falls through to the live createdAt when updatedAt
  is present but unparseable, instead of discarding both.
- Docs: the hidden-catalog "excluded without partial" promise is scoped to
  sessions whose ownership can be read; the retention rationale describes
  the sidecar projection topped up from the bridge and trimmed to the cap;
  the user guide's list of partial causes includes a chats directory the
  daemon could not list.

* fix(serve): key the bridge top-up by PR number and ignore restore time

- The bindings route tops a listed row up from the bridge by PR number
  alone, as the session list's merge does: the sidecar writers replace an
  entry by number when it is re-bound to another repository while the
  bridge keeps the replaced entry, so the repository-keyed merge returned
  both and kept delivering the replaced PR's events privately.
- For a persisted session the live createdAt no longer counts as
  activity: the bridge stamps a fresh one on restore, so merely restoring
  an idle follower could change which session receives a PR's events.
  The createdAt fallback is reserved for a session with no persisted
  record yet.
2026-11-04 23:52:24 +08:00
Shaojin Wen 13e65c1a6e feat(web-shell): link GitHub PR/Issue targets in channel events (#16226)
* feat(web-shell): link GitHub PR/Issue targets in channel events

The channel events page showed the repository#number target of GitHub
journal events as plain text, so operators had to copy the reference into
a browser to inspect the PR or issue behind an event. Chat prompts and the
PRs panel already link out; this closes the last unlinked surface.

The URL is synthesized in the client from the channel config and the
journal fields the GitHub adapter already records, so existing journal
history becomes linkable without any daemon or journal change. The origin
mapping mirrors the adapter's web origin derivation (github.com, GitHub
Enterprise /api/v3, ghe.com data residency), and links pass the same
safety and desktop-opener routing as chat links. Non-GitHub channels,
removed channels, and malformed references stay plain text.

* fix(web-shell): link GitHub events of channels inherited from user settings

The events page resolved link metadata from the management listing, which
is scope-local: a channel configured in user/system settings with cwd
pointing at the project (the setup the GitHub channel guide recommends)
has journal rows on the page but no listing entry, so its targets stayed
plain text.

The journal route now attaches non-secret link metadata (platform type and
API base URL only) resolved from the merged settings the workers actually
run with, filtered by the same cwd-ownership rule the worker grouping uses
so one workspace never sees another's channel metadata. Resolution failure
degrades to unlinked rows instead of failing the journal read. The page
falls back to this metadata when the listing has no entry, and a source
contract test now ties the client origin mapping to the adapter's so the
deliberate mirror cannot drift silently.

* fix(web-shell): make channel event link metadata safe and authoritative

Review of the journal-metadata round found the default settings loader
leaking the queried workspace's .env into the daemon's process environment
(and able to rewrite a corrupt settings file) on a read-only polling route,
and the client preferring the scope-local listing over the merged metadata
even though system settings override workspace values.

The route now resolves metadata through readSettingsSnapshot on the
runtime's effective environment — side-effect-free, corruption throws into
the existing degrade path — and the helper requires that view instead of
defaulting to loadSettings. The page prefers the journal's merged metadata,
keeping the listing as the older-daemon fallback, derives the displayed
number from the thread exactly like the URL does, and strips trailing
slashes from the base URL on both sides of the origin-mirror contract.
The serve protocol doc now documents channelLinkMeta.

* fix(cli): resolve channel link metadata against the boot environment snapshot

The serve process-env guard flagged the journal route's live process.env
fallback: a read-only polling route must not couple to shared mutable
process state. Thread the daemon's frozen boot-time environment snapshot
through the route deps and use it as the parent-process fallback, the same
source the session host resolution already uses, instead of reading the
live environment or adding a guard exception.

* fix(web-shell): emit the resolved GitHub web origin from the daemon

Review round 2 flagged that the web-origin mapping had two owners pinned
only by a source-scraping mirror test, and that the boot-environment guard
had no test able to fail. Retire the second owner: the channel package now
exports resolveGithubWebOrigin as the single mapping owner, the adapter and
the journal route both use it, and the route emits the resolved origin as
channelLinkMeta.webOrigin so the browser only concatenates. The client's
baseUrl derivation remains solely as the older-daemon/listing fallback, and
the scraping contract test is replaced by a mapping-owner table (including
whitespace-padded env-resolvable values), route origin assertions, and
emitted-origin consumption tests.

Pin the boot-snapshot guard behaviorally: mount() accepts a daemonEnv
override and a new case resolves channels from a boot home that disagrees
with the process environment — reverting to a live process.env read now
reds that case. Also call the exported resolveChannelOwnerCwd instead of
re-implementing the ownership predicate, and correct the protocol doc's
read-scope wording for the added settings snapshot.
2026-11-04 23:24:13 +08:00
Shaojin Wenandverify-round e2d3243863 fix(web-shell): express the PR watch events dependency in the channel editor (#16236)
* fix(web-shell): express the PR watch events dependency in the channel editor

The GitHub adapter rejects a non-empty prWatchEvents unless
deliverToBoundSessions is enabled, but the editor never expressed that
dependency: operators could tick watch events, press Save, and receive a
raw English daemon refusal that read as "the selection cannot be saved".

Group the bound-delivery switch and the watch-event checkboxes into one
framed panel, disable the checkboxes with an inline hint until the switch
is on, and mirror the runtime rule as a client-side validation backstop
for hand-written stored configs that carry events without delivery.

* fix(web-shell): keep stored watch selections clearable while delivery is off

Review follow-ups on the bound-session panel:

- A stored selection stays uncheckable-while-gated no more: with delivery
  off, selected events (including unknown stored values) can be cleared
  directly, while new selections remain disabled, so turning delivery off
  never strands the operator between a dependency error and disabled boxes.
- Turning the delivery switch on clears the stale requiresBoundDelivery
  error instead of leaving the instruction under the now-on switch.
- The disabled hint moves into FieldShell's existing hint slot, restoring
  the "description is the first paragraph" invariant; the hint gets an id,
  aria-describedby on the checkbox group and a persistent polite live
  region so screen readers hear why the boxes are gated.
- The render gate now carries the same github scope as the validation
  mirror, and checked-but-disabled rows keep the selected border.
- Tests: shutdown path, stale-error retirement, aria association, and a
  stored delivery-on mount; visuals scenario seeds the bound-session
  fields and captures the gated and enabled states.

* fix(web-shell): scope dependency-error retirement to the dependency message

The switch-on clear deleted the whole prWatchEvents error slot, wiping the
higher-precedence invalidOption message while an unknown stored value was
still ticked; and unticking the last selected event — the other remedy —
left the dependency alert over an empty, valid selection. Retire only the
dependency message, from both remedies, and pin the live region's
persistence (empty region mounted before the hint, same node across the
transition) plus the hint-less rendering of the pre-existing hint slots.

* fix(web-shell): retire watch-events errors by re-deriving validity

Round-4 review follow-ups, all in this PR:

- R3-1: replace the string-matched dependency clear with a per-field
  re-validation on edit, so unticking an unknown stored value retires the
  invalidOption instruction it satisfies (and a remaining valid event swaps
  the slot to the still-unsatisfied dependency message).
- R3-3: submit now scrolls the first erroring field into view even when it
  is not an advanced field; a stored watch-events refusal below the fold is
  no longer invisible.
- R3-2: pin the empty-panel guard with EN/zh absence assertions on a
  non-GitHub descriptor.
- R3-4: table-driven parity test feeds the plugin's own descriptor and
  config through both the editor mirror and the daemon validator
  (test-only devDependency, never bundled); a lone change on either side
  now fails the table.

Each fix carries a mutation witness; 286 channel tests green.

* fix(web-shell): re-derive only shown errors, in both switch directions

Round-5 review follow-ups, all in this PR:

- R4-1: refreshErrors re-derives only the error keys already on screen, so
  a remedy retires exactly the instruction it satisfies without surfacing
  validation the operator never submitted (submit stays the only population
  site), and the delivery switch re-derives in both directions so a shown
  instruction is never silently dropped. New tests pin pre-submit silence
  and shown-message survival across switch toggles.
- R4-2: narrow plugin.management once at module scope in the parity test so
  the optional contract member stays TS18048-clean if test files ever join
  the package typecheck, with a clear error instead of a load-time TypeError.
- Deferred note from the same round: pin the data-disabled styling hook on
  gated rows (dimmed) versus clearable stored rows (selected styling).

Each fix carries a mutation witness; 288 channel tests green.

---------

Co-authored-by: verify-round <verify-round@local>
2026-11-04 14:42:07 +00:00
jinye 98ef7b2cd3 fix(managed-agent): reject dangling Hosted cleanup references (#16275)
* fix(managed-agent): reject dangling Hosted cleanup references

* fix(managed-agent): honor JSONL framing for cleanup admission

* test(managed-agent): align CWD migration lock witness
2026-11-04 13:25:20 +00:00
qqqys e838c4c0d5 test(acp): expect the scheduler's plan-mode notice on the shared loop (#16305)
* test(acp): expect the scheduler's plan-mode notice on the shared loop

With experimental.unifiedAgentLoop on, ACP's shared tool batch fails a call
that plan mode blocks with the scheduler's notice, the one the model gets in
the Ink TUI and headless, instead of ACP's own. The nightly unified leg
failed on the integration case that pinned ACP's wording.

- The integration case expects the notice of the loop it runs on, read from
  the variable the nightly leg sets for every CLI it spawns.
- The ACP parity suite gets a plan-mode case: both loops block the write
  before it runs and ask for no approval, and only the notice differs.
- The design doc lists the notice among ACP's intended differences, in both
  languages.

* test(acp): check plan mode leaves the file unwritten and the current loop's tool result

* test(acp): compare the plan-mode case in full apart from the notice

Swap each loop's notice for a placeholder and compare all requests and
updates, pin that both notices point the model to exit_plan_mode, and
check one file reading per run.
2026-11-04 12:46:19 +00:00
ed6760aa2c ci: reclaim qwen-autofix workflow headroom (#16069)
* ci: reclaim qwen-autofix workflow headroom

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-11-04 19:50:51 +08:00
jinye 93fdebb8fb perf(managed-agent): narrow default workspace admission locking (#16283) 2026-11-04 11:32:23 +00:00
d3ef273950 fix(cli): gate project hooks behind an approval dialog (#14748)
* fix(cli): gate project hooks behind an approval dialog

Hooks in a workspace .qwen/settings.json ran at startup with no prompt
whenever folder trust was disabled, which is the default. Gate them like
project MCP servers (#4615): a hash-bound, user-local approval stored in
~/.qwen/hookApprovals.json, asked for in an interactive startup dialog
(ink and OpenTUI) and re-checked on /hooks reload. Headless runs leave
unapproved project hooks unloaded and print a stderr warning.

* fix(cli): address review findings on the project hooks approval gate

- Only a command-line --yolo skips the gate; a workspace can set
  tools.approvalMode itself. disableAllHooks, bare and safe leave it unarmed.
- Key decisions by the project that supplied the hooks, not the live cwd,
  so /cd and --worktree keep approvals; /hooks reload acts only on a
  gated session.
- Treat null and non-runnable hooks blocks as no hooks; validate stored
  status; hash with null prototypes; write the store 0600 with forceMode;
  keep QWEN_CODE_HOOK_APPROVALS_PATH out of project .env files.
- Dialog: sanitize repo-supplied text to one row, show env/headers/
  allowedEnvVars/prompt, cap the list to the available height, Esc
  postpones instead of rejecting; ink /hooks marks withheld hooks disabled.
- Gate the legacy merged-hooks fallback in getUserHooks too.
- Headless warning names the recovery for a rejected block.
- Fix llm.test.tsx stubs, the real-settings reload test and the ACP
  integration tests (pre-approve via a shared helper); add render tests.

* fix(cli): address second review round on the project hooks approval gate

- Hook registry tags a copy of each definition, so an approved block keeps
  its hash in a reused settings instance (ACP daemon)
- /hooks reload re-reads the approvals file; Esc adds a transcript notice
- Truncated hook values are flagged; prompt text gets the full budget
- Dialog keeps at least three hook rows and one row per hook
- Hooks listing and both /hooks browsers show withheld project hooks as
  awaiting approval
- Integration helper keys and hashes through the CLI's own code
- Tests for getSeparatedHookSettings, the headless warning, the project
  .env exclusion, and gate table titles; docs record the hash scope and
  the subagent frontmatter follow-up

* fix(cli): keep hook registry identity; copy project hooks for the gate instead

The registry copy broke managed hook runtimes, which map registry entries back
to their definitions by config identity. Hand Config a clone of the project
hooks block so the settings instance an ACP daemon reuses keeps its hash.

* fix(cli): bind hook approval to the unresolved block; show what runs

- Hash and display the hooks block before ${VAR} resolution so startup and /hooks agree
- Show command env as KEY=value; matcher last; rows cut to width with a marker
- Derive dialog chrome from the wrapped header at the real width
- Neutral "not approved" label (pending and rejected)
- Docs: frontmatter hooks and statusLine are not gated; ACP status has no reason

* fix(cli): read the unresolved hooks block only when the resolved one is gated

* test: approve project hooks in new tests that rely on them

* fix(hooks): bind approval to referenced env values and harden the approval dialog

- Hash and show the values of $VAR/${VAR} a project hook references, so a .env change re-asks
- Read the approvals file fresh, merge on save, and keep hooks withheld when a save fails
- Count only hooks the registry would run; show a prompt hook's model; cut by code point
- Keep the dialog inside the list budget in both renderers, modal over shells in OpenTUI
- Esc dismisses a block for the session; label rejected hooks separately in /hooks
- Re-arm the gate when a --yolo session leaves YOLO; forward the store path into sandboxes

* test(serve): allow the hook approvals path in the sandbox env guard

* fix(hooks): bind project hooks approval to the workspace env sources

- Hash the unresolved block with the workspace's own .env files and settings.env instead of parsing $VAR names from the hook text; the dialog no longer shows env values.
- Gate and list hooks from the resolved block, worded from the unresolved one.
- Re-gate a --yolo session on its first switch out of YOLO.
- Skip the gate under a shell execution sandbox; pass separated hook scopes from the hosted and memory-import callers.
- Approval dialog strings are English-only in both renderers.
- Tests for the rejected label, dialog choices, reload ordering, function hooks and the absent headless warning.

* test(integration): hash project hook approvals with the workspace env sources

* fix(cli): keep Ctrl+C out of focused Ink shell input

Fixes the foreground-shell flake in #15814. Same change as #15859.

* test(managed-agent): stop the verification suite running out of heap on hosted runners

Raise the surefire fork to 40% of RAM and make the lease renewal test wait for renewals instead of counting them in a 150ms window. See #15248.

* fix(cli): re-gate project hooks against the live block when leaving YOLO

The YOLO-exit listener re-applied the startup approval snapshot, so a block
replaced by /hooks reload while YOLO-exempt ran unreviewed after leaving
YOLO. Read the live block through a provider closed over the live
LoadedSettings (the disabledSkillNamesProvider pattern), and keep listening
so re-entering and leaving YOLO re-gates again.

Closes review finding R7-1.

* fix(cli): keep hooks approval options visible on short terminals

The dialog chrome never clamped to the row budget, so on a short terminal
the fixed-height frame clipped Reject and left only the pre-focused
Approve. Cut the description to the rows the chrome and one list row leave,
in both renderers, before shrinking the hook list.

Closes review finding R7-2.

* test(cli): build reload approval fixtures from the real env sources

The fixture pinned an empty env while the production hash reads every
project env file findEnvFiles discovers, including ancestors of the temp
root, so the approval tests went red on runners with an ancestor .env.
Read the same sources in the fixture and plant an ancestor .env so the
hash coverage stays provably sensitive on every machine.

Closes review finding R7-3.

* fix(core): approval is execution authority for project hooks (either-or)

The interactive folder-trust gate merged from main swallowed the
approval decision: with the feature off (default), Approve persisted
but getProjectHooks() still returned undefined, so the dialog promised
execution it could not deliver (review blocking item 1; tmux Cases
1b/7). readProjectHooks() now admits the block when the hash-bound
approval is approved OR the folder is explicitly trusted; pending and
rejected stay withheld, and an ungated interactive session still needs
folder trust. Tests pin interactive + feature-off + approved => runs,
pending/rejected => withheld, ungated untrusted => withheld.

Also from the same review (suggestions): the Ink /hooks browser now
mirrors the execution accessor instead of showing trust-withheld hooks
as enabled; a command containing variable references says it resolves
them from the approved project env; the Esc notice points at restart
instead of implying /hooks can decide.

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

* fix(cli): align hook status surfaces and docs with either-or authority

The either-or execution gate left three status surfaces behind (review
blocking items 1a-1c): the startup trust warning fired for approved
blocks that actually load, getTrustWithheldProjectHooks() re-listed an
approved block as trust-disabled (duplicate contradictory rows in the
OpenTUI /hooks browser and ACP status), and the Ink browser's enabled
expression showed ungated untrusted sessions as enabled. The warning now
skips approved blocks, the listing accessor shares the execution
predicate (hasApprovedProjectHooks), and the Ink row mirrors
getProjectHooks() directly. Docs state approval-or-trust authority and
the folderTrust-on-untrusted no-dialog case (blocking item 2).

Tests: approved + default trust => no warning and no withheld copy;
ungated untrusted => Ink row disabled; pending => trust row present.

* fix(cli): warn from the execution accessor, not the approval flag

The round-10 warning guard read gated && !pending as "approved", but
pending === false also means "nothing to approve" (an untrusted folder
with the trust feature on contributes no settings). That suppressed the
one signal the user gets in exactly the case the warning exists for
(review blocking item; also the Integration Tests no-AK failure). Skip
the warning only when getProjectHooks() actually serves the block, and
offer approve-or-trust only while a decision is genuinely pending.

Rename the ambiguous core predicate to isProjectHooksApprovalCleared and
document both meanings; getWithheldProjectHooks reads the raw block so
the listing names a pending/rejected decision even when trust also
withholds, with the approval reason outranking the trust reason and no
duplicate rows. Scope the docs' YOLO execution claim to non-interactive
runs.

Tests: folderTrust-on untrusted warning regression (unit + the existing
interactive integration case), approval-reason priority in the listing,
raw-block reachability while pending.

---------

Co-authored-by: 易良 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-04 19:24:58 +08:00
jinye 572ee4f6ba fix(managed): Reap Git descendants before terminating parents (#16298) 2026-11-04 11:23:07 +00:00
jinye b3e9cc7c4d fix(managed-agent): use current state for approval changes (#16296) 2026-11-04 10:24:07 +00:00
jinye 33e3d39d50 fix(managed-agent): use current state for approval changes (#16296) 2026-11-04 10:23:52 +00: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
jinye 26ed1a322b fix(managed-agent): enforce strict SQL mode per embedded connection (#16288) 2026-11-04 17:27:30 +08:00
callmeYe 01bbffb17f fix(cli): derive context tool details from declared schemas (#16292) 2026-11-04 09:21:38 +00:00
qqqys 776331b1f9 fix(mods): skip hooks on ui.backdrop instead of failing (#16289)
A Mod that hooked ui.backdrop failed to initialize, which stopped a headless run before any model request. It is now skipped with the startup line, like other events Qwen Code does not run yet. $.session.version() reports 2.1.318.
2026-11-04 09:12:59 +00:00
callmeYe 641e258546 fix(daemon): automatically discover config Skills changes (#16262) 2026-11-04 09:12:02 +00:00
Shaojin Wen 815c1d3768 feat(web-shell): collapse channel editor tuning behind advanced settings (#16239)
* feat(web-shell): collapse channel editor tuning behind advanced settings

The shared channel editor renders every descriptor field flat, so a fresh GitHub instance shows roughly sixteen controls and the fields needed for first setup are buried under rarely changed tuning. Move the tuning fields into one collapsible advanced panel and hide the default message route until a route exists, keeping the default view to identity, credentials, conversation isolation, and access control.

Collapsing never hides state: the panel opens automatically when any advanced field is configured, shows a count badge while collapsed, and forces itself open when a collapsed field fails validation. Save and validation semantics are unchanged because both already iterate every descriptor field regardless of rendering. The watch-events dependency UX is left to #16236 so the two changes stay complementary.

* fix(web-shell): forward refs through the collapsible wrappers

Review of #16239 showed the three new wrappers accept Radix props including ref, but plain function components drop external refs on React 18, and the repository supports both React 18 and 19. Use React.forwardRef for the root, trigger, and content, pass each ref to its Radix primitive, and pin the contract in react18-ref-compat.test.tsx with both forward-ref type and DOM-ref assertions; a plain-function mutation turns exactly the three new type assertions red. Record the contract in both design documents as well.

* fix(web-shell): keep endpoints and operators visible, make collapsed errors reachable

Review round 1 on #16239 found the disclosure went too far in three places and left dead scaffolding behind. Primary endpoints (baseUrl for GitHub Enterprise / self-hosted GitLab, wsUrl for WeCom) decide where a token is sent, so they stay in Connection instead of collapsing; a fresh enterprise channel could otherwise save without its endpoint and send the token to the public API. Operators is a permission grant and stays in Access control, a deliberate product decision rather than a consequence of the key set.

The panel title becomes a real h3 wrapping the trigger (ARIA makes button descendants presentational, so the previous span-in-button was invisible to heading navigation), and a rejected submit now scrolls the first erroring advanced field into view instead of only opening the panel below the fold. The record rule is unified into one exported predicate shared by validation, the save path, and the visibility/auto-open readers; the unreachable routing section scaffolding is removed; a comment records why enum keys must stay out of the advanced set.

Each new mechanism carries a mutation witness (endpoint placement, heading, boolean arm, record arm, scroll), and the visuals scenarios now anchor the heading, the configured-instance auto-open, and the count badge in a real browser. Both design documents are revised in lockstep.
2026-11-04 16:46:45 +08:00
Shaojin Wenandverify-round 30079d6c3d test(managed-agent): ratchet persistence and runtime-broker class sizes (#16284)
The store decomposition (#16090, #16142) and the broker extraction cluster
turned the god classes into facades, but nothing stops them regrowing. Add
ClassSizeRatchetTest, the class-size gate the structure review left as an open
question, introduced now that the splits have landed. It pins every current
class >= 800 lines in persistence/runtime-broker at its present size
(ratchet-down-only), caps all classes at a global ceiling seeded at the current
maximum, forces any class reaching 800 lines to be pinned, and rejects stale
pins. Verification-only: no production code change. Each rule is mutation
verified; the bilingual design and the structure-review ledger (open question
resolved, acceptance criterion concretized) are updated in the same change.

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

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

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

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

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

Wait for the card's continue control to be enabled, give dispatch a
CI-sized budget, and fail with the card and status state when nothing is
dispatched. The next nightly then names the cause instead of `undefined`.
2026-11-04 16:43:03 +08:00
Lanre Shittu fb8aa7fa3f fix(core): accept images for Qwen2-VL, Qwen2.5-VL and QVQ models the catalog does not list (#16247) 2026-11-04 15:42:53 +08:00
Lanre Shittu 64b499f42a fix(core): recognize Hugging Face TGI context overflow errors so reactive compaction runs (#16245) 2026-11-04 15:42:38 +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
qqqys 0c93377886 feat(mods): run prompt.submit hooks in the OpenTUI terminal (#16195)
* feat(mods): run prompt.submit hooks in the OpenTUI terminal

The OpenTUI live turn now raises prompt.submit for each prompt the person
types, before its row, the input history, @ references and the send. A
rewrite stands in the sent prompt, the row, the input history, the
provenance and turn.start; images stay attached and a paste kept whole stays
collapsed. A drop shows its reason and hands the prompt back to the
composer. While a Mod hooks prompt.submit, queued prompts wait for the turn
to end instead of steering it, so they pass the hooks too.

* fix(mods): keep a returned prompt while a dialog hides the OpenTUI composer

A prompt the prompt.submit hooks drop, or one Esc cancels without a
composer to settle with, went back through the composer handle. A dialog
opened mid-turn (/settings, /theme and other commands that run while a
turn responds) unmounts the composer, so the handle was empty and the
prompt, never shown or recorded, was lost. It now waits on the entry's
composer slot and returns when the composer mounts again.
2026-11-04 07:35:50 +00:00
jinyeandQwen-Coder 14a5a2d98d fix(core): preserve Gemini thought signatures for tool replay (#16270)
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-04 06:53:01 +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
Shaojin Wen 5f0474abc4 test(integration): compare the ACP parity workspace home to its pre-run listing (#16266)
The guard hardcoded the two names the shared harness seeds into the workspace
home, so the next artifact that harness seeds would red every parity scenario
with a message about a runtime leak that is not there. Snapshot the listing
before the agent starts and compare against that instead, which states the
invariant without copying the fixture contents of another module.

Carries forward 1923e184 from #16254, which was pushed after that PR merged.
2026-11-04 13:41:50 +08:00
jinye 4e9c2ad739 perf(managed-agent): narrow output, lifecycle, and storage lock scopes (#16268)
* perf(managed-agent): remove tenant guard from answered watermarks

* perf(managed-agent): publish inline tool results without tenant guards

* perf(managed-agent): share read-only storage admission locks

* perf(managed-agent): queue lifecycle claim updates on Sessions

* refactor(managed-agent): avoid duplicate storage admission reads

* refactor(managed-agent): remove redundant outer monitors

* docs(managed-agent): record narrower lock scopes and validation
2026-11-04 13:10:03 +08:00
Bob.qwencodeandgithub-actions[bot] 7d5a82443a chore(release): v0.26.7 [skip ci]
* chore(release): v0.26.7

* docs(changelog): sync for v0.26.7

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-04 04:49:30 +00:00
jinyeandClaude Opus 5.5 651bd66b36 perf(managed-agent): queue journal commits on the Session row instead of the tenant lock (#16256)
* docs(managed-agent): design journal commit without the tenant lock

Journal commit queues on its own Session row instead of the tenant
placement guard. The design covers why each guard reader stays
correct and converts missing-key locking reads on the commit path.

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

* perf(managed-agent): probe commit-path keys before locking them

Resource writes and hosted cleanup references probed a key that is
often missing with FOR UPDATE, which gap-locks an index shared across
the tenant's Sessions. Probe with a plain read, lock only rows that
exist, and let the unique key decide a racing insert with a locking
re-read and the existing identity checks. Safe with the tenant guard
still in place; it prepares commits of different Sessions to run at
once.

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

* perf(managed-agent): queue journal commits on the Session row

An ordinary journal commit took the tenant placement guard first and
held it to the end, so every commit in a tenant ran one at a time.
It now locks its own Session row as its first statement: lifecycle
begin, settle and Harness placement lock that row before they write,
so none of them can land mid-commit, and the plain reads after it see
what they committed. The storage fence check keeps its alias and
directory locks without the guard. Lifecycle-authority and CSI-pinned
commits still take the guards.

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

* fix(managed-agent): lock the Session row before the storage fence in publication lanes

The publication writer and tool-result lanes checked the storage fence
(alias and directory rows) before locking their Session row. Turn
admission, Session commands and journal commits now lock the Session
row first without the tenant guard, so the old order could close a
cycle on one Session. Both lanes keep the placement guard and check
the fence after the Session row and before the head.

A commit also reuses the CSI pin its first root lock read, so the root
is locked once per commit as the journal lock contract expects. The
design records both changes and notes that commits sharing one storage
still queue on its alias row.

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-04 03:14:22 +00:00
易良 d24198e866 fix(ci): repin the RHEL 10 rpm root to the current stream10 digest (#16261)
The pinned manifest for the RHEL 10 build root no longer resolves on
quay.io: `quay.io/v2/centos/centos/manifests/sha256:99b8fe27…` answers 404,
so every job using that container dies at "Initialize containers" with
`manifest unknown` before a single step runs (run 41759710053, job
126345260531). The tag still exists, and resolves to
sha256:511d3584… today.

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

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

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

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

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

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

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

Three review findings on the resume loop:

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

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

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

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

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

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

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

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-11-04 10:53:11 +08:00
qwen-code-dev-botandQwen-Coder 6af04ec960 test(integration): keep ACP parity runtime state out of the workspace (#16251) (#16254)
* test(integration): keep ACP parity runtime state out of the workspace (#16251)

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

* test(integration): pin the ACP parity workspace home listing (#16251)

Pin the whole workspace home listing so a leaked `usage/` directory fails
alongside `tmp/`, and state that each run gets its own scratch runtime
instead of claiming a shared one.

---------

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

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

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

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

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

Review round 2 findings on #16211:

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

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

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

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

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

---------

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

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

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

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

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