Commit Graph
11817 Commits
Author SHA1 Message Date
9dde2f657d fix(web-shell): surface Managed session list failures (#16082)
* fix(web-shell): surface Managed session list failures

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-11-02 06:16:31 +08:00
Shaojin Wen 7b4b353750 test(managed-agent): gate the two-server first release refusal (#16089)
Extend HostedMcpRestartFaultGatesIT with the FG12d cell #15750 still
carried: a Session configured with two MCP servers, the first release
refused before forwarding, and the same-id retry releasing both.

The case adds fixture2 only for its own boot, waits for both
configurations to settle, and requires three release controls: 503,
the same operation id answered 200, then the other configuration's
release answered 200. Both fixture processes must exit.

The Hosted fork moves by the case's measured 16.2 s, booked at 32 s.
2026-11-02 02:46:13 +08:00
jinyeandQwen-Coder 87c1918a6a refactor(managed-agent): Extract runtime execution dispatcher (#16084)
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-02 01:21:15 +08:00
e3c7a6d7ef fix(core): don't treat fd duplication (2>&1) as a file write in shell permission checks (#15681)
* fix(core): don't treat fd duplication (2>&1) as a file write in shell permission checks

A redirection written without a space whose target is `&` plus a
descriptor number or `-` (`2>&1`, `1>&2`, `>&2`, `2>&-`, `<&0`) duplicates
or closes a descriptor; bash opens no file. The check recorded it as a
write to a file named `&1` in the working directory, so in non-interactive
mode, where edit and write_file are denied unless allowed, read-only
commands that use `2>&1` were refused.

`>&file` and `1>&file` were also checked against a path literally named
`&file`, so a deny rule for `file` did not match: with `Bash(*)` allowed
and writes to `protected.txt` denied, `printf TXT >&protected.txt` was
allowed. They are now checked against `file`. `2>&file`, an ambiguous
redirect in bash, is still treated as a write.

Fixes #15600

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

* fix(core): keep a quoted or escaped & in shell redirect targets

The tokenizer removes quotes and backslashes before redirects are parsed,
so `>"&1"`, `>'&1'` and `>\&1` reached the descriptor branch as `>&1` and
lost their write, although bash writes a file named `&1` for them. With
`Bash(*)` allowed and `Edit` denied they changed from deny to allow, and
`>"&protected.txt"` was checked against `protected.txt`.

The tokenizer now records which characters were typed unquoted. A token
counts as `>&`, `N>&` or `<&` only when its `>`/`<` and `&` were both
unquoted; quoting the word after the `&` doesn't matter to bash
(`>&"1"` still duplicates fd 1). A `$` or backtick word after the `&`
keeps the literal `&word` write, since it may expand to a descriptor or
a file name.

The same trim also dropped a quoted trailing `&` (`>"x&"` was checked as
`x`, and `>"&"` produced no write at all). Only an unquoted trailing `&`
is trimmed now.

Reported in review of #15681.

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

* fix(core): read an unquoted - after >& as close, and a quoted one as a file

After an unquoted `>&`, `N>&` or `<&`, bash reads an unquoted `-` as
"close the descriptor" and starts a new word after it, so
`rm >&-notes.txt` closes stdout and removes notes.txt, and
`cat <&-notes.txt` reads it. The parser took `-notes.txt` as the
redirect word, and since it starts with `-` the flag check in
looksLikePath dropped it: no operation at all, so with `Bash(*)` allowed
and `Edit` (or `Read`) denied, the command was allowed. `main` reported
a write to `&-notes.txt` instead, which a blanket deny still caught but
a deny rule for notes.txt did not.

A quoted or escaped `-` (`>&"-x"`, `>&'-x'`, `>&\-x`) is part of a file
name: bash writes `-x`. The same flag check dropped that write too.

The tokenizer now splits an unquoted `-` right after an unquoted `>&`,
`N>&` or `<&` into its own close token, and the rest of the word goes
back into the argument list. Descriptor redirects are handled in one
place; a `-` that is still at the start of the word there was quoted, so
the write is reported without the flag check.

Spaced forms (`>& -notes.txt`, `>& "-x"`) are unchanged: the tokenizer
trims the unquoted trailing `&` of `>&`, as on `main`.

Reported in review of #15681 (R1-1, R1-2).

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

* fix(core): keep the whole >&-word as a file operation

4ef50aae47 split an unquoted `-` after `>&`, `N>&` or `<&` off as a
close token and passed the rest of the word to the command, as bash
does. But only the command decides what that word is, and the parser
doesn't know every command; for the ones it knows it drops numbers,
`$` words and `{}` words as arguments. So `tee >&-2` (bash writes `2`),
`tee >&-$HOME/.bashrc`, `cat <&-2`, `my-tool >&-notes.txt` and
`rm -- >&--x` reported no operation at all, and with `Bash(*)` allowed
and a bare `Edit`/`Read` deny they were allowed. `main` reported the
whole word (`&-2`) and denied them.

The redirect token now keeps the whole word and reports it as `main`
does (a write of `&-word` for `>&`/`N>&`, a read for `<&`), and the rest
of the word still goes to the command as an argument, so
`rm >&-notes.txt` is still checked against a notes.txt deny rule.

Also adds tests for `>&`/`<&` aimed at `/dev/null` and `/dev/tcp`
(R2-2), for `printf X >&->&"-x"` (N8) and for `2">"&1` (M6).

Reported in review of #15681 (R2-1, R2-2).

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

* fix(core): walk >&- chains in a loop, keep quoted ) in names

The `>&-word` split in pushToken called itself once per `>&-`/`<&-`
group in a word, so a single long token (`rm ` + `>&-` x 6000 + `x`,
18 KB) threw "Maximum call stack size exceeded" out of the permission
check; `main` returns normally. Each level also reported the whole rest
of the word as a path, so the check grew quadratically (389 ms for 5000
groups vs 9 ms on `main`). The split is now a loop, and only the first
redirect in a chain reports the whole word, as on `main`: the closes
after it open no file. 20000 groups now take about 60 ms.

A quoted or escaped trailing `)` was still trimmed from a word, so
`printf TXT >"notes)"` was checked against `notes` while bash writes
`notes)`. The trim now treats `)` like `&`: only a bare one is shell
syntax. `(echo hi >out.txt)` still reports `out.txt`.

hasAbsolutePathTokenForOperation didn't know `>&`/`<&`, so an absolute
`>&/tmp/out.txt` after `cd $DIR` stayed cwd-dependent (an `ask` where
`>/tmp/out.txt` gets none). It now matches `[12]?>&` and `<&` first.

Reported in review of #15681 (R3-1, R3-2, R1-3).

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-11-02 00:51:14 +08:00
ae7901552e fix(core): settle the loop reminder delivery at the model-acceptance boundary (#15559)
* fix(core): settle the loop reminder delivery at the model-acceptance boundary

takeLoopReminder() cleared the pending map and logged the 'reminded'
telemetry while the ToolResult request was only being assembled. Between
the take and turn.run(), the send still awaits microcompaction and
memory recall and yields pending goal events, so a consumer that stops
the generator there loses the streak's warning (dropPendingReminders
only unmarks still-pending keys) while telemetry credits a delivery the
model never saw.

The delivery is now only prepared at the take: the client commits it
when the first model-accepted event arrives and abandons it — restoring
the pending entries — in the send's finally when the request never
reached the model. A full reset drops a prepared delivery so a late
abandon cannot leak a stale warning into a new prompt. The agent-core
round loop keeps its take-time accounting with an explicit commit since
it has no consumer-visible suspension between the take and the model
call.

Fixes #15537

* fix(core): defer the agent-core loop reminder commit to model acceptance

* fix(core): drop the ineffective pre-round reminder abandons

The three abandonLoopReminderDelivery() calls at the abort/maxTurns/time
checks could not change anything: each is followed by a break out of the
round loop, the detector is method-local, and abandon only moves entries
between two private maps without logging. They made the take-site comment
and the maxTurns control read as if they were what keeps `reminded`
truthful, so drop them and say what the control actually pins — no round
crosses a model-acceptance boundary, so nothing commits.

Also, from the same review round:

- abandon's doc block named the wrong function. A retry drops the streak
  through resetToolCallCount(), which deletes the three hard-coded keys;
  dropPendingReminders() cannot reach entries already taken out of the
  pending map.
- client.test.ts asserted not.toHaveBeenCalled() immediately after
  mockClear(), which cannot fail. Compare the call count with the value
  captured before the send instead, so the "suspended before turn.run"
  premise is actually pinned.
- cover the multi-entry path: commit credits every entry of a delivery and
  abandon restores all of them, not just the first.
- telemetry.md and both languages of the loop-protection design doc now
  say `reminded` is logged at model acceptance, and the "sent, never
  accepted, credited on a later send" case is added to the enumeration of
  not-counted cases.

---------

Co-authored-by: suzhiguo7 <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-11-02 00:43:23 +08:00
易良andQwen-Coder dfeae12de0 fix(core): use the amend delta for loosely spelled amend commits (#16073)
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-02 00:43:20 +08:00
Shaojin Wenandverify 4a5c487681 test(managed-agent): gate the expired-cursor resync answer (#16076)
* test(managed-agent): gate the expired-cursor resync answer

FG9e now advances the Session stream's replay floor past a lagging
client and requires one `agent.session.resync_required` frame on both
the public and WebShell streams, with no event id and the floor and
snapshot cursors it must reload through. The same cursor on the plain
events route answers `409 cursor_expired`, and the floor itself stays
readable.

This closes the expired-cursor cell the FG8 tracker (#14163) deferred
and #15750 carried. The case measures 21.3 s locally; the Hosted fork
budget and Verify step move with it.

* test(managed-agent): wait for projection coverage before advancing the floor

Awaitility treats a lambda that returns without failing as satisfied,
so the coverage guard must assert rather than return. Without this a
poll that lands before the lead's Turn is projected would skip the
advance and dereference a null window.

* docs(managed-agent): correct the resync case's tracker references

FG9e now carries the third case, so the relay-hold sentence must name
only the two resume cases. The FG8 record no longer predicts a slice-2
landing and points at the FG9e delivery instead.

* ci(managed-agent): record the resync case's measured range

The 60 s fork-budget entry was written from one 21.3 s run; the final
audit runs measured 21–23 s, so state the range the budget covers.

* docs(managed-agent): fix the stale step-ceiling header and pin note

The Hosted job's opening parenthetical still summed 291 while the tail,
the Verify step and the job ceiling had moved to 292/202/325. Also say
the replay-floor flag is a default pin, not something currently off.

---------

Co-authored-by: verify <verify@local>
2026-11-02 00:28:04 +08:00
jinyeandQwen-Coder 1bdad7d4b7 refactor(managed-agent): Inject store dependencies through constructors (#16061)
Make dependency references final while preserving optional collaborators, lazy cycle resolution and standalone construction. Migrate verification fixtures and add a synchronized bilingual design.

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-01 16:21:00 +00:00
Shaojin Wen cea333133c fix(managed-agent): correct the child result mirror on consumption (#16074)
* fix(managed-agent): correct the child result mirror on consumption

classify() mirrors the child_run delivery into ma_delivery at
classification time. A parent's consuming revision can land after that
snapshot, leaving a CHILD_RESULT row at `delivered/accepted/pending`
forever while the record itself reads `consumed`; FG10b's
awaitConsumed then times out even though the walk completed.

The parent's own consumption commit now corrects that row in the same
transaction, and classification reads the record row with FOR UPDATE
so a consumption cannot slip between its read and the mirror write. A
row not written yet is left to classification, which reads the
record's current state.

Adds focused H2 coverage for the mirror correction and the commit-path
wiring. Issue: #15744.

* fix(managed-agent): lock only the child run's record row

The classification's locking read matched the session scope without the
record's primary key. MySQL therefore scanned the Session's records and
took next-key locks over the rows that scan touched, so an unrelated
child run's commit in the same Session could wait behind a
classification that had nothing to do with it.

Name the record key in the lookup. A real MySQL IT holds the
classification open and proves the unrelated record still commits; the
same IT fails with the session-scope lookup under a lock wait timeout.
Issue: #15744.
2026-11-01 22:39:10 +08:00
2aa908dc70 fix(managed-agent): block ledger-less agent gap fills and pin the A6 refusal arms (#16015)
* docs(managed-agent): mark the B2 identities and live SSE arm packaged

The G3 recovery-matrix status paragraph still listed B2's attempt/stream
identities and live SSE view as remaining focused/integration evidence after
PR #15993 packaged them into the model-round arm, and its admission-count
clause predated the arm's existing admission audit. Both languages now name
the packaged B2 observations and link the B2 design doc; the remaining list
keeps only A3's rewritten-input/Hook half, A6 and R2.

* fix(managed-agent): block ledger-less agent gap fills and pin the A6 refusal arms

A6's first clause is an evidence-semantics rule: unknown producer evidence
must block, never be laundered into proven absence. Two gaps violated its
evidence surface. The Hook/MCP saved-batch ownership refusal — the only
backstop for the extensions-enabled continuation entry — had zero tests:
deleting its clause kept every existing suite green (demonstrated before
pinning). And the parked-round gap fill answered a stranded agent call with
"the call never ran" when the child ledger was unavailable, although only a
present ledger's missing record is positive never-started proof (admission
commits before dispatch); child-lane config/version drift between generations
makes that arm reachable with a live child.

The fill now throws the family's retriable recovery error for an owed agent
fold with no ledger; journaled answers keep their silent replay and non-agent
calls keep their honest fold. Three ownership cases (hooks, mcp,
hooks+extensions) and four gap-fill cases pin the surface; each deletion
control reds exactly its named case. Focused regression 894/894, typecheck,
ESLint and Prettier clean. Bilingual design doc records the A6 evidence map
and scope boundaries; the parent matrix status notes the landed focused
evidence.

* fix(managed-agent): widen the A6 ledger guard and exempt the terminating routes

R1 review fixes, each verified against the code before fixing:

- The guard covers the whole child-run ledger family (agent || workflow),
  matching the module's own family readers; a stranded workflow call with
  an unavailable ledger no longer receives the fabricated never-ran fold.
- An abort exemption keeps the pre-aborted cancel and interrupted-settle
  fills terminating with their pre-A6 abandonment folds; only the
  live-signal continue route blocks. Without it the guard wedged cancel in
  a deterministic failure loop (regression witness demonstrated at route
  level).
- Two route-level cases build the child-lane drift into the replacement
  load (suppressChildAgents on the version-0 orphan parked shape):
  continue blocks retriably (409) with no fabricated fold, cancel still
  settles.
- The owner-gate matrix drops the vacuous hooks+extensions row (the
  harness entry's extensions clause is recorded as a follow-up) and gains
  both gate directions: a stale-revision commandStep refusal and a
  matching-revision acceptance that keeps the A3 commandStep follow-up
  working.

Nine delete-controls red exactly their named cases on a quiet tree with
known-good baselines; focused regression 8 suites 1583/1583, typecheck,
ESLint and Prettier green. Bilingual design docs name all three fill
callers, the residual trade-off, and the narrowed G2 scope.

* fix(managed-agent): extend the A6 ledger rule to every never-ran writer

R2 review fixes, each verified against the code before fixing:

- The resumed-turn answer (hosted mode) and the command-wait gap fill now
  refuse a ledger-less family call on a live signal, mirroring the agent
  fill; their terminating callers are pre-aborted so cancel and settle
  keep terminating.
- Every ledger-less family fold on the terminating paths — the pre-aborted
  fills and the 'all' settlement — writes a dedicated unknown-outcome
  answer (executionStatus unknown, record retained) that claims nothing,
  never "never ran" and never "cancelled".
- CHILD_LEDGER_TOOL_NAMES becomes the single family-membership fact; the
  six inline two-name literals across recovery and the tool turn now read
  it, so a third ledger-backed tool joins in one place.
- The owner-gate matrix gains the extensions row its comment claimed: a
  Hook owner with extensionConfig and no commandStep is refused, and the
  extensions-exemption mutant reds exactly that row.

Five delete-controls red exactly their named cases on a quiet tree with
failure-list adjudication; focused regression 8 suites 1587/1587, ESLint,
Prettier and typecheck green. Bilingual design doc records the round.

* fix(cli): preserve cancel escape for A6 recovery blocks

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

* test(cli): pin journal-only workflow ledger refusal

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

---------

Co-authored-by: 易良 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-01 22:21:47 +08:00
jinyeandQwen-Coder 456a49c14f refactor(managed-agent): Separate runtime broker execution workflows (#16029)
* refactor(managed-agent): Separate runtime broker execution workflows

Give process-local Binding and Session state a registry and separate execution reconciliation, takeover scans and cancellation while preserving the existing fenced state transitions and dispatch boundary. Add a deterministic cooldown completion-order regression and synchronized bilingual designs.

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

* style(managed-agent): Remove extra blank lines from runtime workflows

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-01 13:08:45 +00:00
qqqysandqqqys 91a6274075 fix(web-shell): move Workflow controls into the composer menu (#16047)
* fix(web-shell): move Workflow controls into the composer menu

* fix(web-shell): keep the Workflow menu entry on one line

* fix(web-shell): preserve Workflow access in the VS Code composer

---------

Co-authored-by: qqqys <[email protected]>
2026-11-01 11:07:13 +00:00
1ac4f6cd1b fix(sandbox): install fonts-dejavu-core in the runtime image (#16032)
The capture card renderer in scripts/verify-capture.mjs requests DejaVu Sans Mono, but the runtime image installs no font packages at all.


Patrol-Run: qwen-issue-patrol/jmvx4obxsrz

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

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

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

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

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

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

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

---------

Co-authored-by: yiliang114 <[email protected]>
2026-11-01 10:53:33 +00:00
Shaojin Wenandverify 6e49388c4a docs(serve): refresh daemon usage guidance (#16056)
Replace obsolete alpha-era deployment and persistence limits with current behavior.
Document authenticated background startup, token sidecar lifecycle, and service-manager choices.

Co-authored-by: verify <verify@local>
2026-11-01 17:01:13 +08:00
6bef5a01c6 feat(web-shell): add "Add to Split View" to session menus (R1 fixes for #14878) (#15956)
* feat(web-shell): add sessions to split view from the sidebar

* fix(web-shell): validate split targets and retain pending additions

* fix(web-shell): report transient split-classification failures per session (#14775)

* fix(web-shell): harden the first split open: race, per-id memo, pending mirror, failure notice (#14775)

* fix(web-shell): disable no-op split adds, clarify the capacity hint, document split (#14775)

* fix(web-shell): fix split classification type errors (#14775)

* test(web-shell): fix split pending-navigation and archived-offer test refs (#14775)

* style(web-shell): prettier-format the split classification memo type (#14775)

* fix(web-shell): adapt controlled pane reveal to prop-derived pane set

The main merge (#14577) derives paneIds from the sessionIds prop in
controlled mode, so the reveal effect could no longer sync local state
(setPaneIds does not exist) and its render-time ref check was dead: the
ref is current by the time the effect runs. Track the previous commit in
an effect-time ref instead. Also restore the test-block closer the merge
dropped from SplitView.test.tsx.

* Fix split-persist e2e spec: assert disabled state for already-open session (bot R2 blocker)

The first smoke test clicked 'Add to Split View' on Session A after it
was already a split pane. The R1-10 gate disables that menu item with
the title 'This session is already open in Split View' (sidebar
splitAlreadyOpen), so Playwright retried a click on a disabled item.
Replace the click with toBeDisabled() + title assertions, reusing the
shape of the six-pane spec in the same file. The no-op guarantees
(pane count, draft, DOM persistence) are kept as-is.

---------

Co-authored-by: luozongmin <[email protected]>
Co-authored-by: Сергей Панченко <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-11-01 16:52:54 +08:00
Shaojin WenandQwen-Coder 417e81ec4d fix(review): keep prose re-review requests aimed at the PR (#15998)
* fix(review): keep prose re-review requests aimed at the PR

A re-review request typed as prose ("有更新了,重新评审") carried no
target the parser could classify, and the parser did not refuse it:
every non-flag, non-number, non-URL token classifies as a file target,
so the sentence became a file path, and prose typed beside a PR number
stole the target while the number was demoted to extraTokens.

- parse-args: PR-shaped tokens now outrank file-shaped ones regardless
  of arrival order; an explicit ./<number> spelling stays a file target.
- review skill: a Step 1 intent-routing clause resolves a prose request
  to the PR the conversation names (non-empty inputs only, flags kept,
  the CLI args file preserved), and the description plus the resume
  verdict bullet exclude the updated-PR case from --resume.
- fetch-pr: the head-moved resume refusal now says the fresh run IS the
  re-review and names the plain invocation for next time; the
  restart-bound semantics are unchanged.
- docs: both --resume surfaces in the user guide carry the exclusion.

Pinned by 12 new parse-args cases (the key one red before the fix), a
SKILL.md pin test, and a fetch-pr stderr assertion; DESIGN.md records
the incident as "The prose re-review request".

* fix(review): fence the prose routing's local and posting boundaries

The review round on #15998 caught two defects in the new routing clause
and one regression behind its non-blocking nit:

- Option-only invocations (--fix, --effort low) matched the routing
  condition and would have been retargeted into the conversation's last
  PR, disarming --fix. The clause now names the bare and option-only
  shapes as non-intent and leaves them to the parser verbatim; two new
  parser cases pin that contract.
- The clause promised posting authority for a --comment carried through
  the resolution, but the write gate re-parses the CLI's verbatim record
  on purpose, so a prose record ends at submit with `unbound`. Posting
  is now the resolution's hard boundary: present the resolved invocation
  and ask for an explicit re-run instead of completing a review whose
  submission is already refused. A new submit-suite test pins the
  refusal beside the explicit re-invocation's acceptance.
- The PR-shape reorder demoted a malformed URL behind the assigned
  target and lost its specific diagnostic to the generic extra-argument
  line. The invalid-url refusal is hoisted ahead of the target-assigned
  branch, position-independent, pinned by a dedicated test.

DESIGN.md records the review round; the PR body carries the same
boundaries in both languages.

* fix(review): guard prose routing against local-write intent, qualify scoping

The deep-verification round on ef86b03a99 measured one more hole in the
option-only class and one overclaim:

- F1: "review my local changes --fix" is prose with no classifiable
  target, so it still matched the routing trigger and resolution would
  disarm --fix. The clause now parses the verbatim input first and never
  resolves a PR into a verdict with fix.effective true — a mechanical
  guard, not a wording guess; prose asking for a local review is
  excluded even without --fix. The hash form's copy ambiguity is closed
  too: a # prefix is reference decoration, not part of the target. Four
  new parser cells pin the guard input, the PR-target contrast, the
  hash form, and the ./ escape-hatch boundary.
- F2: delta scoping on a plain re-review is high-only; the docs (both
  surfaces), the resume bullet, the routing clause, the --resume help,
  and the head-moved stderr now all carry the high-effort qualification.
- The ./ escape-hatch wording gains the measured precision: it holds
  when no PR-shaped token is present, not unconditionally.

DESIGN.md records the verification round; the PR body carries the same
boundaries in both languages.

* fix(review): honest resume guidance, promoted-number warning, pre-fix framing

The /review lane's round on 6a5372d6e4 filed five suggestions; all five
verified against the code and addressed:

- R1-1: the routing rationale described the prose-steals-target behavior
  in the present tense — behavior this PR's own parser preference closes.
  It now marks that half as pre-fix and keeps the live half (prose alone
  dies at capture) as the rationale; pinned.
- R1-2/R1-3: the head-moved guidance was gated on a refusal a re-review
  after a completed round never reaches (assessResume checks no-report
  before comparing heads, and cleanup deleted the report), and "gained
  commits" misdescribed a rebase. The guidance now covers the re-review
  family (head-moved, no-report, worktree-gone) with cause-appropriate
  wording; the restart-cost sentence stays with head-moved alone, the
  only reason recordRestart runs for. Docs and the resume bullet carry
  the same completed-round reality.
- R1-4: positive AND negative pins — guidance present on head-moved and
  no-report (without the restart sentence), absent on diff-hash-mismatch
  and worktree-dirty, so dropping the reason gate goes red. R1-3 and
  R1-4 contradicted each other on no-report; the family split resolves
  both (recorded in DESIGN.md).
- R1-5: a bare count inside a sentence is indistinguishable from a PR id
  and the promotion flips --comment to effective. The parser now warns
  when a bare number is promoted with non-PR tokens after it in the
  original order (the canonical number-at-end rescue stays silent), and
  the clause treats the warning as ask-before-running, never posting
  silently. Four-shape test pins presence and absence.

* fix(review): withdraw posting authority from promoted numbers, honest guidance

The sandboxed CI round and the second verification round proved the
promoted-number warning was disclosure, not a fix, and measured four
more gaps:

- R2-1 (Critical): a promoted count carried --comment posting authority
  to a PR the sentence never named ("--comment the 2 follow-ups to
  15998" authorised posting to PR #2). comment.effective is now false
  whenever the PR target is a promoted bare number (promotedNumberTarget
  marks the verdict); the write gate refuses with a promotion-specific
  cause. An explicitly typed number or a PR URL keeps its authority.
- R2-2: the warning moved after the URL preference, decided from the
  actual target, fired exactly when a requested posting authority was
  withheld — demoted numbers and courtesy particles stay silent.
- R2-3: the routing guard keys on what the user spelled, not the
  verdict's target type (prose parses to a file target — the old guard
  excluded the only input it existed for); local-intent moves are
  executable; the hash form has an operational resolution.
- R2-4: trailing sentence punctuation no longer demotes a PR number;
  classification normalises the mark for the integer test only.
- R1-2/R1-3 (fix-induced): worktree-gone gets its own cause-neutral
  sentence (it requires a surviving report); no-report asserts no
  cause; every arm carries the anchor condition; the restart-cost
  sentence prints where recordRestart runs, in the past tense; the
  ruling promise is scoped to high/medium.

New pins: the Critical's exact witness at the gate, both word orders,
the setting-driven arm, worktree-gone, and the reverse-order
malformed-URL cell that reds under a full revert.

* fix(review): name the source of withheld posting authority

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-01 16:50:22 +08:00
Shaojin Wen 859fd0b39c fix(core): keep the whole Kimi K3 generation at a 1M context window (#16048)
* fix(core): recognize the whole Kimi K3 generation at a 1M window

The K3 generation is served as `kimi-k3`, `kimi-k3.1-pro` and, from Kimi
Code, as the bare `k3.1-pro` / `k3.1-pro-highspeed` aliases. Two gaps left
the K3.1 tiers with the wrong window:

- the curated table only matched ids starting with `kimi-k3`, so the bare
  aliases fell through to the 256K Kimi fallback;
- the models.dev catalog is consulted before that table, rounds the 1M
  window up to 1,048,576, and lists each K3.1 sub-version under both its
  dotted and dashed spellings, so the catalog value overrode the declared
  window and compaction thresholds fired too late.

Match the generation once and share that pattern between the curated table
and the catalog correction, replacing the single-id catalog entry that the
next sub-version would have slipped past.

* fix(core): keep Kimi's k3-256k out of the K3 1M window rule

Kimi Code serves `k3-256k` at 262,144 tokens alongside the 1M `k3` and
`k3.1` tiers, and the generation pattern swept it up: with no catalog entry
it resolved to 1M instead of the 256K family fallback, and the
generation-wide catalog correction overwrote a correct cached 262,144.

Name the variant ahead of the generation row, exclude it from the shared
pattern so the catalog correction cannot raise it, and pin its window in the
id-keyed corrections for a catalog that omits or overstates the entry.

Fixes the P1 on #16048.

* fix(core): give Kimi's bare K3 aliases the same modalities as the prefixed ids

Kimi Code serves the K3 generation without the `kimi-` prefix, but the
modality table still matched only `kimi-k3`, so `k3.1-pro`,
`k3.1-pro-highspeed` and a bare `k3` fell through to the text-only Kimi
fallback while their `kimi-` spellings advertised image and video input.

Match the prefix optionally, and name the 256K variant ahead of that row:
Kimi Code reports image input only for `k3-256k`, no video.

Addresses the R1-1 suggestion on #16048.
2026-11-01 08:47:39 +00:00
Shaojin Wenandverify 7ab044d608 docs(readme): add daemon mode quickstart (#16053)
Explain how qwen serve supports shared sessions and browser/API clients.
Document lifecycle commands and loopback authentication risks.

Co-authored-by: verify <verify@local>
2026-11-01 08:22:29 +00:00
Shaojin Wen 0f91cdedae fix(verify-pr): count suite runs and skip notices on closed PRs (#16050)
Follow-ups deferred on #16013:

- A round whose only checks were existing suites had a total of 0
  once suites moved to gates, and the checker refuses that. The skill
  now counts each suite run as one assertion, as it already did for a
  mutant run.
- Gates that record no results (an empty list, or entries counting
  nothing) no longer silence the size warning. When no suite ran, the
  round says in the report what its count holds.
- The blank-suite rule is JSON whitespace on both sides. jq's \S and
  JS's trim() disagreed on U+0085 and U+FEFF, so the checker accepted
  a list the publisher dropped.

Also, a round can queue for hours: the publisher no longer posts a
notice on a PR that closed meanwhile, unless it is finalizing its own
live status. Four such notices landed on #16013 after it merged.
2026-11-01 07:55:09 +00:00
93231beca5 test(managed-agent): assert A3 sibling call binding (#15995)
Co-authored-by: wang1408 <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-11-01 06:57:12 +00:00
2SinceJuly 0d90204b14 fix(cli): keep a long /btw answer readable in a bounded viewport (#15982)
* fix(cli): keep a long /btw answer readable in a bounded viewport

The side answer rendered as unbounded markdown in the bottom controls
region. A long reply pushed the composer off-screen, and because the
interactive CLI paints the viewport itself by default there was no
native scrollback to fall back on, so the clipped rows were
unreachable. The panel now gets a viewport-derived row budget, opens on
the first line of the answer, and scrolls with the wheel, Shift+Up/Down
and PageUp/PageDown while it is open. The transcript list yields those
inputs to the panel so one gesture cannot scroll both surfaces.

Screen-reader and transcript-replay renders keep the full answer.

* fix(cli): keep the transcript scrollable while /btw is pending

The transcript yielded scroll ownership as soon as a `/btw` item existed, but
the answer panel only mounts its scroller once the answer arrives. Nothing
owned Shift+↑/↓, PgUp/PgDn, Ctrl+Home/End or the wheel during that pending
window, which is the common case because `/btw` is usually asked mid-turn.

Yield only when the panel really has a viewport, i.e. while its answer is not
pending.

Pass the answer row budget only in VP mode as well: the panel's ScrollableList
subscribes with `bypassVpGate`, so budgeting rows on the non-VP main screen
armed SGR mouse tracking there and cost the terminal its native scrollback.
2026-11-01 06:48:01 +00:00
Kun De beaa46b172 feat(web-shell): add conversation activity view (#16022)
* feat(web-shell): add conversation activity view

* fix(web-shell): preserve sidebar attention and activity undo lifecycle

* ci: rerun activity validation after main synchronization
2026-11-01 13:04:08 +08:00
e26f377a28 chore(release): v0.26.5 (#16035)
* chore(release): v0.26.5

* docs(changelog): sync for v0.26.5

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Shaojin Wen <[email protected]>
2026-11-01 12:40:25 +08:00
Shaojin Wen 835b080f85 test(autofix-pr): run a watch's helpers in a chain against a fake gh (#16017)
* test(autofix-pr): run a watch's helpers in a chain against a fake gh

Add the integration test the design listed as not written yet. The
helpers run one after another as the skill runs them, through the CLI's
parser, against real git (a bare remote, the user's clone, the watch's
worktree) and a fake gh first on PATH, which they spawn as they would the
real one. The gate runs as `schedule` printed it, split by a real shell.

The fake answers from a JSON description of the PR and reads the PR's
head from the bare repository, so a push moves it. It answers only the
fields a GraphQL query selects, checks the host and the Int PR number,
and logs anything it does not know, which fails the test even when a
helper swallows the failed call.

Five cases: a watch from start to merge (with a reply whose answer was
lost), a push by someone else during a round, a re-run spent then
refused, the per-check fix budget across real pushes (one head's result
asked by commit), and the author's push access. Each also checks the
writes made to GitHub, that pushes are fast-forwards onto the PR's
branch, and that the user's checkout is unchanged.

* test(autofix-pr): check the field behind a GraphQL alias in the fake gh

The fake read an alias's response key and never the field it stands for,
so `root: nonexistentField(first: 1)` was answered like the real
`root: comments(first: 1)`. It now records the field and its arguments
behind each alias and answers only the one alias it renders. The message
for a query it does not answer no longer calls every such query a paging
one.

* test(autofix-pr): pin the conflict seam and the watch's refs in the chain test

- The fake answers `root` only through its alias: selected bare, as
  `root { ... }` or `root(first: 1)`, it is a field GitHub does not have.
  It also refuses a run with no job instead of answering it as a pass.
- The user's checkout is compared with its branches and worktrees too,
  and only `.qwen/tmp` is left out of its status, so a branch or a file
  the watch leaves behind shows. The re-run and fix-budget cases end
  with the cleanup a stop runs.
- A sixth case leaves a conflict for a person: `reply` records it, the
  gate stays quiet on that head and on the head the watch pushes next
  while GitHub still answers UNKNOWN, and someone else's push brings it
  back.
2026-11-01 10:50:41 +08:00
Shaojin WenandQwen-Coder 06c1f98971 fix(verify-pr): count test suites apart from the round's assertions (#16013)
* fix(verify-pr): count test suites apart from the round's assertions

Rounds folded whole workspace suites into assertions.json, so the
published "Scripted assertions" line measured the repo, not the round:
37 of the 252 counted reports in the 30-day census exceeded 1000, and
one round's 7737 held a few dozen checks of its own, the rest being
five suites counted again for every mutant run.

assertions.json gains gates: [{ suite, pass, fail, skipped? }], one
entry per existing test suite run as a targeted gate. The skill says
suite results and mutant runs stay out of pass/fail/total, and that a
gate red the PR causes still reaches the verdict as an assertion. The
checker refuses a malformed list and warns when over 1000 assertions
come with no gates. The publisher prints the gate totals on a line of
their own, numbers only, and does not let them decide the verdict.

Also, deferred from #15938: the paragraph for a merge-ready round with
failures now states the checker's rule instead of inviting the reader
to excuse the failures, and the artifact-check guard's empty-value legs
are pinned.

* fix(verify-pr): warn on empty gates and mirror the checker's shape rules

Address the three stage-2 triage suggestions: an empty gates list no
longer silences the suite-sized warning; the publisher's jq refuses
blank suite names and non-count skipped values (false/null) instead of
defaulting them, so a shape the checker refuses never renders totals;
and the design docs say 'all three' / '三者' where the rule count grew
from two to three.

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

* fix(verify-pr): give the gates shape rule one witness and honest totals

Address the five R1 review suggestions:
- one shared fixture table drives both the publisher's jq and
  checkArtifacts, and both bound counts at 2^53-1, closing the 1e309
  divergence where jq clamped an overflow the checker refuses
- the skipped clause prints only when some entry records the field,
  so an unrecorded count is no longer published as a measured zero
- a trusted verdict over gate reds carries one advisory sentence in
  both languages pointing at the assertion ledger; still verdict-inert
- pin the skill's compensation sentence and name the gates-size
  warning in the local clear-list and the CI-path sentence
- bump the workflow size baseline to 375110: the parity comment, the
  conditional clause and the advisory exceed the remaining allowance

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

* fix(verify-pr): print the skip clause only for fully recorded gates

Address the two R2 review suggestions:
- the clause now requires every entry to record skipped; a mixed list
  published a partial sum as the run's skip total
- pin the advisory's trusted-verdict guard from the false side: an
  untrusted run with gate reds prints the totals but no advisory
- fixtures: a mixed list drops the clause, a recorded skipped:0 keeps
  it, and a timeout headline over gate reds stays advisory-free

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-01 02:26:31 +08:00
e739b9fd3f fix(core): reject LSP queries when no server is ready (#15946)
Definition, reference, hover, symbol, implementation, call-hierarchy,
code-action and workspace-symbol queries returned an empty result when no
server was ready, so the lsp tool reported "No definitions found" while a
server was still starting or had failed. Route them through the same
not-ready rejection that diagnostics already use, naming the server and
its state.

Part of #13143.

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: 易良 <[email protected]>
2026-10-31 18:26:22 +00:00
Shaojin Wen 143a8b0653 ci(sdk-java): run the Hosted admission gates even when the Hosted verify fails (#16010)
* ci(sdk-java): run the Hosted admission gates even when the Hosted verify fails

The admission step had no condition, so it ran only when every earlier
step passed. The Hosted verify fails most nights, and the step was skipped
on all 13 nights from 10-14 to 10-30 that the verify failed, including the
three since the Runtime Broker fault gates stopped being skipped the same
way. It ran on the other three nights and passed each time, in about two
minutes.

The step rebuilds the module with `clean verify` and needs only the CLI
bundle, the installed SDK and the MySQL service. Each gate class works
under its own random tenant, and on green nights it already runs against
the database the Hosted verify used. So it takes the condition the fault
gates and the O4 step use: run unless the run was cancelled, as long as
the SDK install succeeded.

The class check after it reads only the admission step's reports. It now
follows that step's outcome instead of the job status; otherwise it would
still skip after a red Hosted verify while the step it audits ran.

The pin for the fault-gate condition now covers both steps, and a new case
pins the class check's condition and position. The setup-step assertion
now names the offending steps in its message: vitest abbreviates the
array once a step name is long enough, and the install step's own name
already is.

* ci(sdk-java): keep the O4 gates reachable on red-verify nights

The admission step now runs on red-verify nights too, so the job's worst
case sums all four capped steps (291 min) plus the uncapped setup. Raise
the hosted-harness-mysql ceiling from 304 to 324 so a slow setup (the CLI
install retries three times) cannot cancel the job before the O4 gates,
whose !cancelled() condition would record them skipped.

Close the two pin gaps the review found: the red-verify condition test
gains the O4 row (all three conditioned steps are now pinned), and the
admission class-check test pins that the check step carries no
continue-on-error, so a dropped gate class still fails the job.

All three pins are mutation-witnessed at this head (ceiling back to 304,
O4 if: deleted, continue-on-error on the check step each redden exactly
their case); the nine sdk-java.yml parsing suites pass 709/709.

* ci(sdk-java): boot the admission gates on their own database and bump the size baseline

Now that the admission step runs after a red Hosted verify, it must not
share that verify's database. Managed schema validation refuses any table
outside the managed set, and four Hosted ITs create fg*_fire tables they
drop only during cleanup (the relay classes only while their Spring context
is active), while the isolation extension truncates only ma_ tables. A
class killed before its cleanup would leave one behind and fail both
admission gates at boot. On a MySQL 8.4.6 rig, an fg9b_fire table in
hosted_harness_test made the base command fail both classes with "Managed
schema must contain exactly the 38 target tables", while this command
passed both on a fresh qwen_admission_ci. The step now creates that
database the way O4 creates qwen_o4_ci.

The pin test adds the O4 row, pins that the class check has no
continue-on-error, custom shell or altered command, that no gate command
swallows its failure, and that admission boots on its own database. Both
steps' names are checked before use.

sdk-java.yml outgrew the ratchet: main's copy was already 3971 bytes over
its recorded 144802 without a bump, and this change adds the rest. Even
without its comments the file would exceed the ceiling, so the baseline
moves to the file's size.

* test(ci): pin the admission step whole and keep the install ahead of the verify

The admission step's command, env and shell are now pinned with literal
values. The hosted-gates class check counts a failed class as one that
ran, so an `||` fallback, a pipe, or -fn in the command or in MAVEN_ARGS
would have turned a red admission gate green, and a second -Dmysql.url
would have pointed it back at the shared database. A list of forbidden
substrings caught some of these and rejected harmless commands, so the
fault-gate and O4 rows go back to pinning only their condition and
continue-on-error; their commands are unchanged by this PR.

The three gate rows also pin that the SDK install comes before the Hosted
verify. After it, a red verify would skip the install and every gate with
it, on exactly the nights the condition is for.

Comments: the job comment opened with stale ceilings (162 + 20 + 50 + 20
= 252); it now gives the measured setup time (under 10 minutes on every
nightly from 10-14 to 10-30), states that what changed is the timed-out
Verify path, which used to skip admission, and says the CLI install makes
up to three attempts rather than retrying three times. The O4 row comment
says O4 has carried its condition since it was added.

* test(ci): derive the red-verify contract from the job and pin the setup allowance

The red-verify condition was asserted over three hand-listed step names,
so a gate added to the job later sat outside every assertion and a red
verify would skip it silently, as it skipped the admission gates. A new
case derives the contract from the steps after the Hosted verify:

- Only the two report checks that need the verify's output may run on a
  green job alone. Every other step's condition must be `always()`, or
  `${{ }}` around `always()`, `!cancelled()` or `failure()`, optionally
  narrowed by one step's outcome. Any other condition either has no
  status function, so GitHub prefixes `success() &&`, or still reads the
  job status. A narrowing step must itself run regardless of the verify.
- No step after the verify carries continue-on-error.
- Every step that invokes Maven, matched as a word so folded commands,
  wrappers and quoted paths count, carries the exact red-verify condition
  and a ceiling, so it enters the summed step budget.

The job-budget rows now carry the setup allowance (33 for the MySQL job,
15 for failover) and assert ceiling - steps against it, so raising a step
ceiling re-derives the allowance instead of quietly spending it.
2026-11-01 02:25:44 +08:00
2SinceJulyand肖洪进 7838a9c7ee fix(web-shell): expose the Plugins back control without the navigation rail (#15651)
* fix(web-shell): expose Plugins back control without the navigation rail

The Plugins page had no visible exit when the navigation rail was hidden:
opening it closes the compact drawer, the floating menu button is suppressed
while a panel is active, the shared panel header skips plugins, and the
embedded child managers hide their standalone back button. Render a back
control in the Plugins header whenever the rail is unavailable, reusing the
existing return-to-chat callback.

Fixes #15646

* fix(web-shell): keep the Plugins back control through detail views

Render the same back control in the embedded detail breadcrumbs so the
narrow-host exit survives a detail view instead of requiring a trip back to
the list. Give the panel control its own accessible name so it no longer
collides with a managed page back button, and let the narrow header wrap
instead of shrinking the tab group over the workspace picker.

* test(web-shell): assert the narrow header keeps the picker inside the host

The geometry case only checked that the tabs and the workspace picker do not
overlap, so a build that shrinks the tab group instead of wrapping the header
still passed while the picker hung past the host edge. Assert containment too.

---------

Co-authored-by: 肖洪进 <[email protected]>
2026-10-31 18:14:15 +00:00
8a2817ac8a fix(core): keep uniquely named MCP tools when names collide (#15757)
* fix(core): keep MCP tools when a server returns duplicate names

@google/genai aborts the whole tools/list conversion on a single
duplicated declaration, so discovery discarded every tool of the server
and the caller reported "No prompts, tools, or resources found".

Keep the first declaration of each duplicated name in the discovery
adapter, log the skipped names, and let the rest of the server register.

* fix(core): drop every declaration of a duplicated MCP tool name

Two tools that share a name can still be unrelated commands, so advertising
one of them shows the model a schema `tools/call` need not execute. Count
declarations over the complete paginated listing and skip every declaration of
a colliding name, keeping uniquely named tools and reporting the group size.

The traversal now runs inside the adapter call, which also resets the
bookkeeping on a retry and terminates on a repeated cursor instead of letting
the SDK pager spin on pages filtered down to nothing.

* fix(core): bound MCP tool pagination and end the chain on any falsy cursor

A server that mints a fresh cursor per call never repeats one, so the
duplicate filter's walk had no exit; cap it at the 100-tool ceiling the
genai pager used. An empty-string cursor is the pager's end-of-chain
signal, so treat it as terminal instead of re-fetching the last page as
duplicates. Warn when a repeated cursor truncates the listing, and keep an
all-collision server from being reported as an empty one.

---------

Co-authored-by: 肖洪进 <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-10-31 18:12:59 +00:00
2SinceJulyand肖洪进 34d8042988 fix(core): delete the OAuth refresh lock only when owned (#15661)
* fix(core): remove the OAuth refresh lock only while this process owns it

Every SharedTokenManager instance registered a process-exit cleanup that
unlinked oauth_creds.lock unconditionally. A process that only constructed
an OAuth client and then exited normally could delete another process's
live refresh lock, letting a third process refresh the same predecessor
concurrently.

acquireLock() already minted an ownership token and wrote it into the lock
file, but discarded it. Keep that token on the instance, have the exit
handler remove the lock only when the on-disk token still matches, and
clear the token when the lock is released.

The existing cleanup test pinned the buggy behavior; invert it and cover a
non-owner exit, an owner exit, and a lock since replaced by another process.

* fix(core): apply the lock ownership check to release as well

A slow refresh can outlive the stale window, letting another process evict
the lock and write its own token. The release path still unlinked the file
unconditionally, so it deleted the new owner's live lock. Compare the
on-disk token before removing, and keep the token on transient failures so
a later exit signal can retry instead of disarming cleanup.

Add a release-path regression, a real-filesystem round-trip test, and a
write/read equality assertion that fails when the retained token differs
from the bytes actually written.

* test(core): shorten the fake access token in the lock ownership fixture

The 20-character literal was read as a secret assignment by the PR safety
precheck, which blocked automated review until a maintainer approved the runs.

---------

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

Routing battery: gate matrix across all associations, structural pin
against clause creep; association-clause mutation reds the suite.
2026-11-01 00:12:22 +08:00
jinyeandQwen-Coder cf7e77810e refactor(managed-agent): Share the session event delivery loop (#15952)
Share Public and WebShell delivery while preserving replay, permissions, timestamp projections, heartbeats and deletion handling. Keep the session creation command already merged upstream and document the focused stream verification.

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-31 23:04:59 +08:00
Shaojin Wen fe30b5daaa test(managed-agent): pin lapsed-lease ledger writes and the IT's result-store wiring (#16018)
* test(managed-agent): pin lapsed-lease ledger writes and the IT's result-store wiring

Design decision 5 says a scanner whose lease lapsed still records and
settles until another claim moves the fence, but no test held that half:
adding a lease_until condition to any ledger write left every case green.
A new AutomationScannerTest case writes past lease_until with owner and
fence unchanged, then lets another claim take the lease.

WorkspaceCsiSqlWorkerMySqlIT now asserts its journal holds the result
store it wired, so dropping that wiring can no longer leave the
zero-projection assertion passing for the wrong reason.

Decision 6 no longer says "lost its lease", which read as expiry.

* test(managed-agent): cover every fenced ledger write after the lease lapses

The lapsed-lease case now also releases, blocks, records a gate outcome
and settles with a detail after lease_until, so a lease_until condition
on any of the nine owner+fence writes fails it.

The scanner's class Javadoc, the ledger's method Javadocs and the sibling
test name said "lost its lease", which reads as expiry; they now name a
takeover by another claim, as decision 6 does.
2026-10-31 22:07:48 +08:00
Shaojin Wen 74bd584f0b fix(core): report a registry that kept changing as a PeerControllerError (#16007)
* fix(core): report a registry that kept changing as a PeerControllerError

A controller registry mutation that loses its write to a concurrent
writer three times in a row failed with a plain Error. The CLI treats
anything that is not a PeerControllerError as an I/O fault, so
`qwen sessions controllers add/remove` printed "could not write
<path>: ..." for what is contention, not a permissions or disk problem.
A caller branching on `error.code` saw undefined.

RegistryChangedError now extends PeerControllerError with the code
`registry-changed`, and its message names the file the way the sibling
registry refusals do.

The redo bound itself had no test: every rival-write case exercised a
single redo. A new case lands a rival write inside each of five
attempts and expects the third to give up with `registry-changed`,
leaving only the rival's records on disk.

* test(core,cli): pin the registry-changed message and the CLI error branches

Nothing pinned the registry-changed message: dropping the file path,
or rewording it as a disk fault, left the suite green. The case "gives
up, saving nothing, when the registry keeps changing" now expects the
exact message.

That case's comment said a rival write lands inside each of five
attempts, but only three attempts run. It now says what the rival
budget is for: the rival stops after five writes, so a missing redo
bound ends in a sixth attempt that succeeds instead of looping forever.
The previous commit's message repeats the old description. The helper's
parameter is renamed from reads to count.

handleRemove's PeerControllerError branch had no CLI test, so forcing
it either way left controllers.test.ts green. The add and remove blocks
each get a case checking that a registry-changed error is printed as it
stands, and "reports a failed write" checks that an I/O failure names
the file.

Two more comments are corrected. MAX_MUTATION_ATTEMPTS counts attempts
in all, not redos. The CLI comment's list of self-explaining errors now
covers refusals that are not about the label, such as too-many, and its
other branch is said to cover a lock wait that ran out as well as an
I/O error.
2026-10-31 13:45:37 +00:00
Shaojin Wen 7bc00a3a08 feat(review): re-anchor on drift, run actionlint, account blockers, refuse retries (#16001)
* feat(review): re-anchor on drift, run actionlint, account blockers, refuse retries

Four fixes from the 2026-10-31 audit of seven review run ledgers
(#15057, #15377, #15385, #15578, #15853, #15955, #15974):

- Head drift posts the completed review at the new head (the re-anchor
  protocol) instead of abandoning the round — the maintainer's ruling
  on #15578, never landed in the skill until now. The once-per-review
  bound and the COMMENT cap survive; the marker withholds its anchor.
- script-lint runs actionlint's workflow-level checks (syntax, context
  availability, action refs) and gates them as [lint] Criticals; only
  the embedded run: shell stays deferred. #15974's unloadable workflow
  (runner context in jobs.env) is the regression test, measured against
  actionlint 1.7.12.
- pr-context writes a blocker inventory; compose-review cross-checks it
  against the round's blockerRulings and caps at COMMENT when the walk
  skipped a blocker (#15974's missed review-level blocker).
- submit documents --dry-run as the only sanctioned probe and refuses a
  re-post at the commit already landed (already-posted-this-commit) —
  the retry that replied this round's own roots back to themselves
  (#15853). The receipt records lastCommitId.

Each fix carries its rule text, pin tests, and a DESIGN.md incident.

* fix(review): isolate actionlint config and refresh lint on re-anchor

Both Criticals from the #16001 review, reproduced against the exact PR
source and closed structurally:

- The newly active actionlint invocation auto-loaded the reviewed repo's
  .github/actionlint.yaml, so a PR-added ignore-all rule suppressed every
  workflow diagnostic — including the unloadable-workflow expression this
  linter was activated to catch. It now runs against a private mkdtemp
  neutral config via -config-file (measured to override the discovery),
  fails closed when no private config can be created, and carries exactly
  the two trusted compatibility exceptions the repo's own lint script
  runs (self-hosted runner labels, environment.deployment schema lag).
  The regression plants the ignore-all config in a real temporary git
  repository and proves the expression finding survives.

- The re-anchor protocol dropped fixed authored findings but left the
  lint gate regenerating its findings from the old-head report, so a
  lint blocker the delta fixed was republished against the new commit.
  The protocol gains a refresh step (scratch worktree at the new head,
  script-lint --diff rewriting the report bound to the new diff) and a
  lintDiffPath compose field that makes the gate — body and ledger
  marker alike — hash the new diff as its freshness basis. Executable
  regressions pin both directions: fixed blocker gone (COMMENT, absent
  from body and marker), still-standing blocker keeps gating
  (REQUEST_CHANGES). A malformed lintDiffPath fails closed.

* fix(review): base-tree actionlint config, empty-diff parse, lane provisioning

Three findings from the round-3 review of #16001, each reproduced
against the exact source and closed:

- The private {} config blocked PR-planted suppressions but also dropped
  the repo's maintainer-merged compatibility exceptions (ECS runner
  labels, concurrency.queue, the $/ self-repository action reference),
  turning correct release-workflow changes into bogus [lint] Criticals.
  The trust boundary is now the MERGE-BASE tree's .github/actionlint.yaml
  (hex-shape-guarded git show, copied into the private file, {} only when
  the base has none); the head copy is never read, and the two trusted
  -ignore constants ride on top exactly as scripts/lint.js runs them.
  Pinned with a two-commit repo and a real-binary run showing the base
  exception honored while the planted ignore-all stays blocked.

- An empty refresh diff (the new head reverted the PR's last change) was
  treated as a parse failure: the range selection fell back to the old
  plan's context-inclusive hunks and promoted a pre-existing finding into
  a blocker the gate accepted as fresh (the empty diff has a valid hash).
  diffParsed now separates parse success from range count; empty and
  deletions-only diffs classify everything pre-existing. The reviewer's
  exact scenario is an executable regression.

- The lanes that run the gate provisioned no linters, so the new
  uninstalled-capping fired on CI and the real-binary regressions were
  skipIf'd out of every run. The review job and the unit-test lane now
  install the pinned, SHA-256-verified actionlint/shellcheck through the
  repo's own installer and promote the job-scoped dirs onto GITHUB_PATH
  (unconditional, advisory, Windows excluded). The 335 workflow meta-test
  pins pass unchanged.

* fix(review): lint in the worktree cwd, guard lint.js entry, pin the sha fence

Three gaps from the scripted deep verification of #16001 (50/55
assertions, 17 mutants), two of them in the round-3 fixes themselves:

- F1: actionlint resolves the config's paths: entries against its cwd,
  and the runner spawned it in the session checkout, so every
  path-scoped base exception was inert while the global keys kept
  working — the split that hid behind the labels-only test. The runner
  now receives the worktree root and spawns inside it with PWD aligned.
  The real-binary suite gains the path-scoped fixture it was missing: a
  benign concurrency.queue edit that must lint clean, and an attack cell
  where the base exception suppresses the schema-lag finding while the
  planted head ignore-all still fails to hide the expression.

- F2: the provisioning steps imported scripts/lint.js, whose
  unconditional top-level main() ran a full lint pass on import and
  never reached the install loop, silently under continue-on-error.
  lint.js now runs its CLI only when it is the entry point
  (import.meta.url guard, argv[1]-safe for node -e imports), and both
  steps assert each installer produced its binary before promoting the
  dir, so a future no-op install fails visibly instead of degrading to
  skipped tests.

- M15: the option-injection fence was unobservable — git's own
  rejection of a malformed option matches the guard's refusal, so the
  guard's mutation survived. A spawn spy now pins the fence directly:
  non-hex merge base never spawns git; a hex-shaped nonexistent one
  does (positive control with the exact argv).

Also acknowledges the verification's correction: the bot review's
$/prepare-release-runner example does not exist at this head (the base
config entry is precautionary); the queue exception at line 485 is the
one reachable path-scoped case, and F1 is pinned on exactly that shape.
2026-10-31 13:43:34 +00:00
edde639992 feat(cli): support exact-version updates without npm discovery (#11486)
* feat(cli): support exact-version updates without npm discovery

* fix(cli): repair build and close exact-version update review gaps (#11486)

The merge into this branch left two copies of the semver default import
in standalone-update.ts, so packages/cli no longer compiled (TS2300) and
every downstream gate went red. Drop the added duplicate, keeping main's
import position.

Also address the remaining review findings on the exact-version update:

- Verify the extracted archive's manifest.json version matches the
  requested version before activation, alongside the existing executable
  smoke test. The manifest is the field the installation reads back and
  the prepared-update gate compares against; an inflated manifest version
  would otherwise silently freeze all future prepared updates.
- Pin the yargs builder contract: --target-version must reach the handler
  as argv.targetVersion under the production .strict() parser, going red
  if the builder is dropped or the option renamed.
- Add the issue #11484 acceptance-criterion-7 E2E test: update a
  disposable standalone install to an exact version served from a local
  HTTPS release root while the npm registry sink records zero requests,
  asserting the installed manifest version and rollback retention.
- Document that an explicit target is not persisted: automatic updates
  move a standalone install forward again next session, and keeping an
  exact older version requires general.enableAutoUpdate: false.
- Add the Chinese version of the design doc with reciprocal language
  links.

* fix(cli): close standalone update review gaps from round 2 (#11486)

Address the second-round review findings on the exact-version update:

- Validate the extracted archive's full manifest identity (name, target,
  version) before activation instead of version alone. A mirror artifact
  with the right version but no name passed every check and installed,
  after which the directory silently stopped classifying as a managed
  standalone install; the gate now rejects it and preserves the install.
- Guard both version comparisons with normalizeVersion's own predicates
  (SEMVER_RE and semver.valid), so a regex-valid but semver-invalid
  version like 01.2.3 hits the intended mismatch message instead of
  throwing "Invalid version format" out of the comparison.
- Rename the manifest gate's message to name the archive check rather
  than the smoke test, which has not run yet at that point.
- Make the target-version E2E hermetic: pin QWEN_REQUIRE_SIGNATURE,
  QWEN_CODE_LANG, and QWEN_HOME per spawn (witnessed by staging hostile
  ambient values inside the test), re-seed the fixture installation in
  beforeEach so vitest task retries never inherit the post-activation
  tree, and guard the afterAll server-close promises so an early
  beforeAll failure no longer hangs the hook and leaks the scratch root.
- Pin the documented "explicit targets permit same-version reinstalls
  and downgrades" guarantee with performStandaloneUpdate cases that go
  red if the prepared-route staleness check ever moves into the shared
  path.
- Correct the fixture's header comment: local-git-clone classification
  requires the resolved CLI path inside the cwd, not merely a cwd inside
  a git worktree.

* fix(cli): close standalone archive gate review gaps from round 3 (#11486)

Validate the archive's runtime flavor and executable layout against the
installed build before activation, share one version predicate and one
runtime-layout predicate instead of hand-inlined copies, give every gate
predicate a discriminating test, keep the E2E registry sink honest with a
discovery control, and document the manifest identity contract.

* fix(cli): pin the git shim's module scope in the reverse-apply screen test (#11486)

The test shadows git with an extensionless Node shim under tmpdir. Node
derives the module system of an extensionless file from the nearest
ancestor package.json, so a "type": "module" package.json above tmpdir
(the runner image ships /tmp/package.json) loads the CJS shim as ESM:
it dies on "require is not defined" before forwarding to real git, the
restore's ls-files -v spawn never arms the stamp, and the screen test
fails without the screen ever being asked. Pin {"type":"commonjs"} in
the shim directory so the shim loads the way its body is written,
regardless of what sits above tmpdir.

* fix(cli): flavor-aware standalone archive routing and launcher entry-point gate (#11486)

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

* fix(cli): sanitize --target-version rejection echo and bun test fixture

normalizeVersion throws with the raw flag value interpolated, and the new
--target-version error path echoed it to stderr unsanitized, so terminal
escape sequences (OSC-52, C0/C1, bidi overrides) supplied via argv reached
the user's terminal verbatim. Collapse whitespace and strip escapes,
control and format characters at the write site, mirroring the existing
sanitizeForStderr pipeline, and pin it with an escape-carrying reject case.

Also stage the real bun archive layout in the standalone-update fixture
(bun/bin/bun plus the node/bin/node compat shim installBunRuntime writes)
so the smoke test's two-hop execution chain has a witness: shortening the
staged shim now reds the bun-flavor update test with "Smoke test failed".

* fix(cli): accept pre-v0.23.0 standalone manifests without runtime (#11486)

* fix(cli): address target-version review feedback (#11486)

* fix(cli): keep update recovery guidance and gate win32 test (#11486)

Preserve the updater's manual-recovery command by using a 4,096-character bound for its own failure text; the 500-character bound remains for the untrusted --target-version echo.

Gate the pre-v0.23.0 manifest success-path test with the same win32 skip as its sibling tests, since atomicReplace returns 'deferred' there.

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

* fix(cli): keep update echo sanitizer at its call site (#11486)

Move the bounded stderr sanitizer back next to the update command so the shared stdio helper keeps its existing surface, and raise the hosted plan wait to the 10s bound the rest of its file uses so the full-suite gate no longer times out under load.

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

* fix(cli): raise hosted plan wait to the file's 10s bound (#11486)

* test(cli): describe the registry sink's real enforcement (#11486)

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

* fix(ci): stage the standalone archive layout in the runtime harness (#11486)

applyStandaloneUpdate rejects an extracted archive that lacks bin/qwen and
the bundled runtime, but the harness payload carried neither, so
npm run test:standalone:runtimes:ci died in node/clean before any scenario
ran. Stage the layout create-standalone-package.js ships - bin/qwen plus the
bun flavor's node/bin/node mirror shim - so the fixture matches a real
archive and the update scenarios exercise the gate instead of tripping it.

* fix(cli): gate standalone archives on cli-entry.js (#11486)

The pre-activation layout gate required the installer's launcher and runtime paths, but the repository's standalone-runtime harness repacks a runnable archive without them, so npm run test:standalone:runtimes:ci died in node/clean and the required Test job stayed red. The harness lives in maintainer-only scripts/ territory, so relax the updater's archive gate to the documented v0.18.1 floor: lib/cli-entry.js. smokeTest still runs the bundled runtime, lib/cli.js and lib/cli-entry.js before activation, and isStandaloneInstallDir keeps using the full layout for classification.

Revert the harness edit rejected by the footprint gate, and update the design/user docs and the binless fixture test to the relaxed contract.

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

* fix(cli): make launcher-less standalone updates launchable (#11486)

The update gate accepts archives from the v0.18.1 floor without the
installer's bin/qwen launcher (the standalone-runtime harness repacks one
that way), but installation detection and the PATH wrapper still required
it. A successful update could therefore leave a tree that neither launched
nor classified as standalone.

Share one layout contract between the gate and detection: the CLI entry
point plus the bundled runtime. Detection still accepts the legacy
launcher-only layout. When a tree has no launcher, ensureBinWrapper writes
a wrapper that execs lib/cli-entry.js through the bundled interpreter and
refreshes a stale wrapper that targeted the removed launcher. Update the
design and user docs and cover the classifier, wrapper and refresh paths.

* fix(cli): resolve a restartable launcher for launcher-less standalone installs (#11486)

Accepting launcher-less install trees made the daemon's one-click update
arm a restart against a hardcoded bin/qwen that such a tree does not have:
activation succeeded, the restart threw ENOENT, and the update was
reported as failed. Resolve the PATH wrapper the updater maintains, which
lives outside the install and is refreshed when an archive ships no
launcher, and fall back to the archive's own launcher; offer manual
instructions when neither exists. Only a wrapper that names the install
may restart it.

Skip the launcher-less acceptance test on win32 like its siblings: its
fixture is Unix-shaped and a Windows host derives win32-x64, which
validateTarget rejects.

---------

Co-authored-by: 西岐 <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: qwen-code-ci-bot <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: qwen-code-bot <[email protected]>
2026-10-31 13:42:54 +00:00
86f1338b86 feat(serve): batch workspace session live-state snapshots (#12513)
* feat(serve): batch workspace session live-state snapshots

* test(serve): include batch live-state capability in integration baseline

* fix(serve): close review gaps in the batch session live-state route (#12513)

Index POST /sessions/live-state in the REST reference's persisted-catalog
row so the docs contract sees the protocol section, give the batch route the
single-workspace route's server-side records (routing-failure warn on
untrusted members, daemonLog error on unexpected bridge failures), make the
mid-batch disconnect guard observable with an AbortController and a
macrotask yield between members, and move the duplicated member-failure,
generation-currency, and bound literals into workspace-route-runtime for
both plural batch routes. Six tests pin the new witnesses.

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

* fix(serve): align live-state scope docs and pin batch route guards (#12513)

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

* test(cli): pin grouped REST doc rows against bare SDK names and column shifts

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

* test(cli): fail closed on grouped REST doc rows and ambiguous SDK qualifiers

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

* test(serve): pin documented live-state batch bounds against silent drift (#12513)

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

* test(cli): isolate the suite from the operator's settings home (#12513)

The autofix verification gate runs the packages/cli suite with the host's
real HOME on a machine shared with other jobs. A malformed
$HOME/.qwen/settings.json there (observed content: "X") makes every
operator-settings reader fail closed at once — readOperatorSettingsScopes
is designed to refuse a corrupted policy file — and one such file failed
408 tests across 15 files in the measured gate rejection.

Pin a private empty QWEN_HOME per test-file process in the suite setup
when the environment did not select one, following the file's existing
doctrine for operator-owned environment. Tests that exercise QWEN_HOME
set, stub, or delete it themselves. settings.test.ts keys every
user-settings and .env path off a mocked os.homedir, so it now deletes
an ambient QWEN_HOME explicitly; with any ambient value set, 13 of its
tests fail regardless of this change.

* test(cli): harden the QWEN_HOME suite pin and batch maxima test (#12513)

- Pin on truthiness so an ambient empty QWEN_HOME no longer falls
  through to $HOME/.qwen, matching every production reader.
- Sweep the pinned home from afterAll instead of an 'exit' listener
  that vitest workers never fire (51 leaked dirs on src/config -> 0).
- Restore QWEN_DEBUG_LOG_FILE around loadCliConfig tests so the debug
  logger's fire-and-forget writes stop racing the sweep.
- Witness that the suite never resolves settings from the ambient
  $HOME/.qwen, and assert member shape in the 20-selector maxima half
  so twenty failed members cannot read as twenty snapshots.

* test(cli): keep AppContainer store work off the pinned per-file QWEN_HOME (#12513)

* test(managed-agent): settle fresh group leaders before age-judged sweeps (#12513)

A just-forked process can read in ps with procps's negative-elapsed
wraparound (e.g. 441077234-00:18:40), which parsePsElapsed deliberately
leaves undatable. An undatable leader judges every worker/group identity
'unknown', so an unwitnessed sweep conservatively holds the live process
and rejects unproven — flaking the real-process sweep tests within
milliseconds of the spawn. settledGroupTable now also waits for the
leader's row to be datable, and the four sweep sites that judged a fresh
process without the guard use it.

* fix(cli): keep the QWEN_HOME test pin unconditional and alive for the process lifetime (#12513)

The conditional pin honored an ambient QWEN_HOME, so agent-gate vitest
runs read and wrote the operator's live settings home; the teardown
sweep then deleted the live pin while un-awaited extension-store work
was still reading it, turning Test (ubuntu-latest) red on ENOENT. Pin
unconditionally, keep the pin for the process lifetime, reap stale pins
by age at setup, await the floated extensions-list handler, and add
teardown, pin, reaper, and memory-only-contract witnesses.

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

* fix(cli): keep the QWEN_HOME teardown witness inside test-setup.ts (#12513)

The previous commit registered the pin teardown witness as a second
setupFiles entry in packages/cli/vitest.config.ts, which deterministic
verification rejects as CI/verification machinery outside the PR
footprint. Revert vitest.config.ts and fold the witness into
test-setup.ts itself: vitest runs root-level afterAll hooks in reverse
registration order, so the hook registered immediately after the pin
runs last among this file's root hooks and still turns every file red
if a teardown sweep of the live pin is reintroduced below it
(mutation-probed). The separate witness setup file is deleted.

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

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

* test(integration): raise the installed distribution case ceiling

The tui-parity installed-distribution darwin-x64 leg failed on the
npx-lifecycle-enabled no-relaunch-fallback case with "Test timed out in
120000ms" (run 40537979533, job 122316223603). The five preceding cases
averaged ~117s on that host, right at the ceiling: startup alone can
spend the whole 90s ready-prompt budget there (#15677) and the /about
retry window is another 90s, so 120s cannot contain a healthy case.

Raise the per-case ceiling to 240s and keep the budget helper's note in
sync.

---------

Co-authored-by: 西岐 <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: qwen-code-ci-bot <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: qwen-code-autofix <[email protected]>
Co-authored-by: qwen-code-autofix <qwen-code-autofix@localhost>
Co-authored-by: qwen-code-autofix[bot] <qwen-code-autofix[bot]@users.noreply.github.com>
Co-authored-by: Qwen Autofix <[email protected]>
2026-10-31 21:40:40 +08:00
Shaojin Wen 2a8ae9b2df docs(managed-agent): record the ManagedAgentStore setter investigation (#16014)
* docs(managed-agent): record the ManagedAgentStore setter investigation

Complete the P1 inventory with the ManagedAgentStore finding: the seven
public setters are Spring @Autowired(required=false) optional-dependency
injection (four optional, two required, worktreeFiles manual), not ad-hoc
mutable wiring. Converting them to constructor ObjectProvider injection is
a large change to the central 7,700-line class with ~30 test wiring sites
whose correctness depends on Spring runtime assembly the manual-setter
unit tests do not fully exercise. Like the broker reconciliation cluster,
it warrants a dedicated design pass and full integration verification
before implementation rather than an incremental slice.

* docs(managed-agent): count worktreeFiles among the required setter injections

configureWorktreeFiles carries plain @Autowired and its bean is supplied
by ManagedAgentConfiguration.managedWorktreeFiles(); the direct calls in
VerificationFixtures are test setup, not production wiring. The setter
inventory therefore reads four optional and three required, and the
@Autowired(required = false) statement now applies only to the optional
four. Both language versions move together.
2026-10-31 21:40:05 +08:00
80273bd34a fix(core): extend built-in reasoning effort to DeepSeek-V4 and new Qwen prefixes (#15987)
* fix(core): extend built-in reasoning effort to DeepSeek-V4 and new Qwen prefixes

Third-party gateways (PAI-EAS, internal proxies) host DeepSeek-V4 models
under arbitrary hostnames, and the invitation-only qwen-latest-series
previews and qwen4 family speak the same tiered reasoning_effort contract
as qwen3.8-max. The hostname whitelist and exact-id tables left these
routes without any effort controls.

Recognize the deepseek-v4 prefix case-insensitively on any OpenAI-compatible
endpoint (official endpoint keeps its own contract; non-V4 aliases stay
host-gated), and route the qwen-latest-series-invite- and qwen4 prefixes
through the shared tiered-effort wire gate and the stable qwen3.8-max
control tiers. Explicit capability declarations still take precedence.

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

* fix(core): address review on effort prefix recognition

- Normalize xhigh to high for every v4.1-flash gateway alias, matching the
  stable Flash id instead of falling into the legacy xhigh->max rule.
- Carry the tiered Qwen manifest through opaque duplicate-endpoint routes:
  the route preview guard, the live ACP configuration guards, and the
  reasoning-configuration lookup now admit the new prefixes, with
  two-endpoint regression coverage.
- Stand the built-in DeepSeek normalizer down on newly recognized
  third-party hosts whenever an explicit declaration governs (wire reshape,
  display state, and override clearing), preserving declared/static effort
  semantics; established DeepSeek routes keep their shipped interaction.
- Update the duplicate-route DeepSeek assertion to the new contract and
  refresh the stale single-family scopings in docs and comments.

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

* fix(core): scope declaration stand-down to pre-PR routes, keep routed declarations

- Limit the established-route compatibility exception to the exact
  model/host pairs the pre-PR recognizer accepted, so a declaration for a
  newly recognized alias keeps its wire semantics even on a MaaS host.
- Make the stand-down self-determining: a profile-less resolved capability
  is a declaration, and both the effective-reasoning shaping and the
  DeepSeek state stand the built-in normalizer down on newly recognized
  combinations, covering pipeline injection, provider reshape, display,
  and override clearing alike.
- Preserve an already-resolved declaration in the routed Qwen fallback;
  the tiered manifest applies only when nothing resolved.
- Regressions: MaaS-alias static and configured effort, display/wire
  alignment, established-pair boundaries, and a live ACP duplicate-endpoint
  declaration case.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: 易良 <[email protected]>
2026-10-31 13:17:08 +00:00
Shaojin Wenand易良 6eef5d999e fix(cli): bind the PR badge when a --flag is glued to the number (#15989)
* fix(cli): bind the PR badge when a --flag is glued to the number

* fix(cli): count a --repo qualifier glued to the PR number

* fix(cli): narrow the glued-flag grammar to flag-shaped tails

* fix(cli): anchor the glued qualifier and keep /review's strict closure

* fix(cli): recognize --repo anywhere in the command's glued flag chain

* fix(cli): anchor the URL fallback and share one glued-link grammar

---------

Co-authored-by: 易良 <[email protected]>
2026-10-31 21:09:25 +08:00
Shaojin Wen 94448d368d fix(verify-pr): keep the mutation matrix off the tree live cells measure on (#15999)
* fix(verify-pr): keep the mutation matrix off the tree live cells measure on

A local round interleaved its mutation matrix with 80 live daemon cells
on one tree. A build inside a mutation row propagated the mutant into
dist/, and the hand-patched repair still carried the mutant expression,
so every live cell and the whole matrix were withdrawn and the round
shipped as inconclusive.

Require a dedicated tree for the mutation matrix, gate every daemon or
dist-level harness launch on a scripted dist-versus-source anchor check
with a mismatch-reporting control, and forbid hand-patching dist to
repair a contaminated build. Pin the new rule in the workflow meta-test.

* test(verify-pr): pin the launch gate's coverage and control clauses

Review on 0d06209b2d showed the four rig-isolation pins left two
clauses of the launch gate unasserted: deleting "covers every PR
anchor" or the mismatch-reporting control still passed 4/4. Pin both
so a future edit cannot silently weaken the gate.

Also name the clean-dist launch gate in the mutation-runner pointer,
so the section that proves a mutant reached dist and the rule that
proves the measurement tree's dist is clean cross-reference each other.

* fix(verify-pr): define the launch gate's reference side and isolate per-tree artifacts

Review on 21014c30d6 (R1-1/R1-2/R1-3, RC1-RC5): the gate compared dist
against an unnamed source, so a self-consistent mutant build passed it;
"PR anchor" was undefined and collided with the mutation-anchor sense;
the control proved liveness, not refusal; the gate had no carve-out for
the producing row's own run; and tree isolation stopped at the checkout
while the workspace links, the shared sandbox tag, and the single Maven
repository stayed host-global.

Name the reference side as a sha256 manifest of each measurement arm's
clean dist recorded before the matrix starts, retire the undefined
anchor term, require the gate to refuse a known-mutant build, exempt the
producing row's own launches, and extend isolation to node_modules, the
sandbox image tag, and the Maven repository. Replace the fragment pins
with the whole bullet pinned inside its section plus the whole pointer
line; a 17-row mutation acceptance goes red on every row, and a
real-file M8 run turned the suite red before a byte-identical restore.

* fix(verify-pr): qualify the matrix-tree copies and name both sandbox levers

Review on ff867ec035: the cp -al recipe dropped the per-package
qualifier the install strategy already requires (root marked 17.0.1
vs packages/core marked 15.0.12 shadow for real), hardlinks share
inodes so a writing row truncates the round's copy, the sandbox clause
named only the launch-side variable while the build reads
QWEN_SANDBOX_IMAGE_TAG and --sandbox-image outranks the export, and the
pin's section slice failed open when the closing heading was renamed.

Carry the qualifier and the never-symlink rule, require real copies
from the store for rows that write under node_modules, name the build
lever and the flag precedence, and assert the slice bounds. The
acceptance matrix grows to 26 rows, all red on mutation including both
heading renames; a real-file rename run turned the suite red before a
byte-identical restore.

* fix(verify-pr): scope the sandbox image per launch and bound the pin slice by the next heading

Review on b99f52a5e3: <tree> sat where a Docker tag must go and the
export's value was never spelled out; a shell-wide export leaks the
matrix image into every later cell; the two cp -al copies disagreed on
when hardlinks are safe (the base build's postinstall step writes under
node_modules); the slice bound was a named heading, so an inserted
section could shelter a moved bullet; and the prose named a build
variable no test pinned at its read site.

Spell a sanitized label and the full image reference, inject the
variable per launch instead of exporting it, carry the write caveat in
both copies with an explicit copy import method (pnpm's auto falls back
to hardlinks without clones), bound the slice on the next heading, and
pin process.env.QWEN_SANDBOX_IMAGE_TAG in build_sandbox.js. The
acceptance matrix grows to 30 rows, all red on mutation; real-file
heading-insertion and variable-rename runs each turned the suite red
before byte-identical restores. Merged main (#15988, #16002); the suite
now runs 197 tests, all green.
2026-10-31 21:08:39 +08:00
812a8c7ccb fix(release): use the job token for the publish checkout (#15730)
* fix(release): use the job token for the publish checkout (#15727)

The publish checkout only reads the release SHA and disables persisted credentials, so the bot PAT it asked for was never used. Gating a read-only checkout on that secret turns any checkout-time auth failure into a failed release before any work runs; the push step already re-authenticates with the PAT it needs.

* fix(release): probe the bot PAT on dry runs (#15727)

The publish checkout no longer touches the bot PAT, so a dry-run rehearsal
stopped exercising the credential and a rotated PAT surfaced only on the
next live release. Probe it in a dry-run-only, non-blocking step placed
after the push step, where the live path legitimately holds it.

Widen the package-scripts pin so it catches any secret on the read-only
checkout and any CI_BOT_PAT consumer before the push step, not just the
exact step name and secret spelling. Bump release.yml's size baseline for
the added step.

* fix(release): check the bot PAT's push capability on dry runs (#15727)

* fix(release): drop the ineffective dry-run PAT probe (#15727)

The probe only ran on dry runs, where no release step consumes the bot
PAT, and its `|| echo` turned every failure into a green warning. Make
the publish checkout's job token explicit instead, and keep the pins
that no secret or bot PAT reaches it before the push step.

* test(release): pin the bot PAT out of the publish checkout (#15727)

The PR's Reviewer Test Plan names expect(checkoutStep).not.toContain('CI_BOT_PAT'), but neither test file contained it. Add the assertion before the job-token pin so the documented mutation fails on exactly the named assertion.

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

---------

Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: 易良 <[email protected]>
2026-10-31 20:51:11 +08:00
Shaojin Wen f99062525d test(managed-agent): pin the private CSI receipt guards and fix design decision 5 (#16011)
#16000 documented that Sessions created through the private CSI lane
record no tool receipts for projection. Pin the two places that hold:

- WorkspaceCsiSqlWorkerMySqlIT now wires a ManagedToolResultStore into
  the journal, as production does, and asserts that no
  TOOL_RESULT_PROJECTION row exists after its receipt-carrying CSI
  commits. Without the store the capture guard was never reached.
- ToolPublicationStoreTest marks the Session private (through
  tool_profile or runtime_request_key, since csi_guard is generated)
  before claiming, asserts the claim is empty and the row untouched,
  then clears the marker and claims the same row.

Decision 5 of the automation runtime design said a scanner whose lease
expired records and settles nothing. Occurrence writes check owner and
fence, never lease_until, so they are refused only once another claim
moves the fence. Both language versions now say so, matching the
AutomationLedgerStore Javadoc corrected in #16000.
2026-10-31 20:28:07 +08:00
cca888dfcc fix(core): bump MCP client and core to 2.4.0 to drop pre-bundled fast-uri 3.1.0 (#15925)
* fix(core): bump MCP client and core to 2.4.0 to drop pre-bundled fast-uri 3.1.0

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

Fixes #15919

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

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

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

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

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

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-10-31 19:25:36 +08:00
Shaojin Wen c726d2cfc1 docs(managed-agent): close the D1 prose finding; record the reconciliation prerequisite (#16012)
Carry forward the R3-1 review fix that was verified but not pushed before
#15997 merged: the D1 structural-debt prose now matches the table (the
observation cooldown and tool-hold inventory are named as extracted
collaborators, not as concerns still mixed into the class), in both
languages, and the service's hold-inventory javadoc defers the detailed
discharge invariant to RuntimeHoldInventory instead of duplicating it.

Also record the reconciliation-cluster investigation: reconciliation,
recovery scanning and cancellation share the service's three concurrent
maps, binding-lifecycle helpers and CAS settle, so none is a clean
standalone seam. The design now names the prerequisite — a shared-state
registry that owns the maps and lifecycle helpers — to be designed and
reviewed on its own before those paths can be extracted, and before the
M6 stamp-before-slot-drop ordering becomes a pinnable seam-level contract.
2026-10-31 19:18:39 +08:00
Shaojin Wen 1515428391 fix(web-shell): proxy the PR watches route in dev (#16004)
* fix(web-shell): proxy the PR watches route in dev

The dev server's proxy table had no entry for the autofix-pr routes, so
`GET /autofix-pr/watches` fell through to the SPA fallback and answered
with index.html. The PR watches page then failed JSON parsing on open
and reported "Unexpected token '<'". Only the primary workspace's
unqualified route was affected: the workspace-qualified form is already
reached by the `/workspace` prefix entry.

Production is unaffected because the daemon serves the page and the API
from the same origin, with no dev proxy in between. The browser suites
did not catch it because they stub the route at the network layer, which
intercepts the request before it reaches the dev server.

* test(web-shell): fold the PR watches proxy case into the coverage table

Review feedback on #16004: the standalone block repeated the existing
daemon-API coverage table's lookup, request shape and bypass assertion,
and pinned the entry to the Goals route by reference identity. That pin
was arbitrary — nothing requires the two routes to share one object — so
re-objecting Goals with the spread idiom this file already uses elsewhere
would have reddened the PR-watches case while the route itself stayed
correct, pointing a future reader at the wrong entry.

The case is now a row in the existing table, which keeps one enumerated
list of the primary prefixes the dev proxy must serve. Each row carries
the URL the daemon actually serves rather than a derived one. The
load-bearing assertions are unchanged: deleting the proxy entry still
reds exactly this row, and re-objecting Goals no longer affects it.
2026-10-31 11:15:30 +00:00
Shaojin Wen 35daec35ea fix(verify-pr): keep the verdict line a plain paragraph and check the comment footer (#16002)
* fix(verify-pr): keep the verdict line a plain paragraph and check the comment footer

Three verification rounds on PR #15984 shipped their opening verdict as a
single Markdown heading: the round title and the whole ~950-character
verdict paragraph shared one `#` line, which GitHub renders as one giant
H1 — a wall of bold text instead of a readable lead. All three comments
also went out without the attribution footer the publish contract
requires.

The skill only said "verdict line first" without pinning the Markdown
shape, and the consistency checker only checked that the verdict word
and head OID appear in the first ten lines, so both defects passed every
gate.

- The skill now pins the verdict line to a plain paragraph (a round
  title may lead it as plain text, never as a heading), with the
  measured #15984 example and a template.
- Local rounds assemble the exact posted body — report content plus the
  resolved footer — into comment.md, post that file, and read the
  comment back against it byte for byte; report.md stays footer-free by
  contract.
- The checker warns when the lead line is a heading carrying the
  verdict or an over-long heading (GFM indents of up to three spaces
  included), and when comment.md lacks a well-formed attribution
  footer. Warnings, not errors: a cosmetic defect must not make the CI
  publisher refuse an otherwise consistent verdict.
- Pin tests reproduce the shipped shape from the real round-3 comment,
  and the workflow meta-test pins the new skill text.

* fix(verify-pr): accept CRLF line endings in the comment-body checks

Review of #16002 reproduced a Windows-authored comment.md failing the
anchored footer regex: splitting only on \n left a stray \r on the
footer line, so a valid attribution was reported missing and the local
publish gate sent the artifact back for unnecessary repair. GitHub
renders CRLF and LF identically.

Normalize CRLF (and lone CR) to LF before the line-based checks on
report.md and comment.md, and pin LF/CRLF parity: the same valid footer
passes under both endings, a missing footer and a heading lead still
warn under CRLF, and the reviewer's CLI probe is covered end to end.
2026-10-31 11:02:46 +00:00
Shaojin Wen d811674c55 fix(verify-pr): keep each verify round in its own comment (#15988)
* fix(verify-pr): keep each verify round in its own comment

Verify jobs are serialized per PR, but their publishers queue on hosted
runners, so a round's live status is often still "running" when the
next round's verify job starts. The status step took that comment over
and both publishers then wrote into it, so one round's report replaced
the other's. A cancelled pending run's notice could likewise replace
another round's live status.

The status comment now carries a run marker. The status step reuses a
live status only when it is this run's own (a re-run attempt), and the
publisher writes only into that status, posting fresh otherwise.
WEAK_BODY no longer decides anything and is removed.

Also, from the #15966 follow-ups: evidence images are collected from
the round the comment publishes instead of from every round, oldest
first, and the pre-agent sweep comment no longer describes the
head -1 ordering.

* fix(verify-pr): let a re-run publisher find its own comment

The publisher looked only for this run's live status, but the body it
publishes carries neither the running marker nor the run marker. A
publish job re-run after it had posted therefore found nothing and
posted the report a second time. Every terminal body now keeps the run
marker right after the leading verify marker, and the publisher matches
its own run's comment whether it is still the live status or already
the report. The status step still reuses only a live status.

Tests now pin the status body's leading markers in order, use a re-run
attempt number distinct from the run id, check the run marker in every
terminal body, and check that the evidence skip count comes from the
published round.

* fix(verify-pr): never let a notice replace a published report

A publish job re-run on its own downloads its own attempt's artifact,
which the verify job never uploaded, so it always renders "results
unavailable". Since the previous commit matched this run's comment
whatever it held, that notice replaced the round's report. The lookup
now also reports whether this run's comment carries findings, and a
body without findings is posted beside it instead. A notice may still
replace this run's live status or its earlier notice.

Every body now opens with a header written once, before the branches:
the verify marker, the run marker and, when the body carries findings,
the substantive marker, ended by a blank line. The build-failure body's
substantive marker moves up into it. Both lookups that choose a comment
to write into read markers only from the header, so a marker quoted
further down a body is text.

Tests pin the notice/report gate in both directions, the header of every
terminal body and that it is written once, header-only matching in both
lookups, and that the status step does not reuse this run's report.

* fix(verify-pr): read the substantive marker from the header for the snapshot

The previous-round snapshot was the last lookup still matching its
marker anywhere in a body, so a newer notice quoting the substantive
marker below its header would have been snapshotted in place of the
newest real report. It now reads the header like the other lookups.
Build-failure reports posted before the header change carry the marker
below their header and are skipped by the snapshot during the
transition.

Tests add that notice shape to the snapshot case, and render the
reportless body to check it carries no substantive marker.
2026-10-31 18:56:36 +08:00