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

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

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

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

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

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

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

Review follow-up on #16079:

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

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-07 23:42:24 +08:00
Sergey BessonovandShaojin Wen 588db8b137 feat(web-shell): show tool arguments and result in expanded cards (#15435)
* feat(web-shell): show tool arguments and result in expanded cards

Cards outside the built-in detail renderers expanded to the same 80-character result summary shown while collapsed, so the chevron promised details it never delivered. Render the call's arguments and result instead, pretty-printing a JSON object or array and falling back to plain text, with the trajectory inspector's 4000/40 000 caps and a Show more action.

Fixes #15280

* fix(web-shell): address tool card details review findings

* test(web-shell): cover tool card review findings

* fix(web-shell): preserve tool card JSON payload fidelity

* fix(web-shell): sanitize tool payloads and cancelled results

* fix(web-shell): bound tool payloads by render mode

* fix(web-shell): memoize tool card payload formatting

* fix(web-shell): size tool card JSON by render cap and scope its rules

* fix(web-templates): raise document export budget for tool cards

* fix(web-shell): address round 3 tool card review suggestions

* fix(web-shell): gate Show more on escaped payload length

* fix(web-shell): stop rendering the cancelled input dump as a result

A cancelled call that recorded no output carries the daemon's redacted input dump in rawOutput.text, written there as the same string as reason. cancelledOutputText accepted it as output, so the expanded card showed the call's own arguments twice, once as Result. Skip a text candidate that equals reason while keeping output/stdout.

* test(web-shell): pin the arguments escape guard and the inline cap

Both behaviours were unpinned: replacing the arguments-side sanitized-length comparison with the raw length, or lowering MAX_TOOL_PAYLOAD_LENGTH to 400, left the suites green. JSON.stringify escapes C0 bytes but not the bidi controls, so an object argument carrying U+202E is the reachable shape where escaping moves the payload past the cap.

---------

Co-authored-by: Shaojin Wen <[email protected]>
2026-11-07 23:38:51 +08:00
Shaojin WenandQwen-Coder 5b44cfad71 fix(web-shell): include channel sessions in release dialog (#16537)
* fix(web-shell): include channel sessions in release dialog

The release dialog inherited the sidebar's default source filter, which
excludes sourceType: "channel". A workspace filled by idle channel
sessions therefore opened an empty recovery list.

Query the workspace catalog without a source filter for this dialog and
keep the default, channel, and qwen-live rows. The sidebar and every
other useScopedSessions caller keep the default filter.

Add hook/dialog regressions and verify banner -> release -> create with
a real sourceType: "channel" session filling a one-session workspace.

* fix(web-shell): keep live sessions visible in release dialog

Expanding the recovery list from the sidebar's default source filter to
persisted history can still bury the sessions holding slots: closed
channel rows fill the first page before they are reached.

Add a liveOnly session-list option on the daemon route and SDK so callers
can read the workspace bridge instead of persisted history. The release
dialog requests the shared page size with that option and keeps listing
default, channel, and qwen-live rows, so closed history can no longer
crowd out what can be released.

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

* fix(sdk): serve liveOnly session lists over native REST

The ACP session/list mapping drops liveOnly, so a DaemonClient
configured with an AcpHttpTransport silently returned persisted
history for the release dialog's live-only request. Route liveOnly
through the native REST transport for both the legacy and
workspace-qualified list methods, and cover the real ACP transport
with a regression test.

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

* test(web-shell): pin the release dialog's source whitelist

Live-only listings can include side_task, scheduled_task, and standalone
sessions that the server does not hide. Add those rows to the dialog test
so widening the whitelist fails, and state the 100-row live-page ceiling
next to the page-size request.

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-07 22:43:17 +08:00
Jabrail d0b80d4a99 fix(memory): match reader advice to the binding index budget (#15368)
* fix(memory): match reader advice to the binding index budget

* fix(memory): account for writer notices and combined limits

* fix(memory): bound description advice to recoverable index space

* fix(memory): keep uncertain index advice conditional

* fix(memory): preserve durable budget remedies and complete links

* fix(memory): keep regeneration budget advice conditional

* test(memory): align writer integration with reader line-count advice
2026-11-07 14:36:02 +00:00
fd3324b7cd fix(serve): serialize overlapping cleanup retries (#15482)
* test(serve): reproduce overlapping cleanup retries

* test(serve): wait for the first cleanup pass before overlap

* fix(serve): serialize overlapping cleanup retries

* test(serve): cover refused overlapping cleanup retries

* test(serve): capture the retry scheduler through mock contexts

* test(serve): cover cleanup retry guards under contention

* test(serve): keep regression setup within the shared wait budget

* test(managed-agent): isolate H2 catalogs by test class

---------

Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-11-07 22:28:14 +08:00
Shaojin Wen 0b54b9be99 fix(managed-agent): restore two runtime-broker classes to their size ratchet (#16542)
ClassSizeRatchetTest fails on main itself: RuntimeBrokerService is 2947 lines
against a 2943 ceiling and RuntimeBindingProvisioning is 1327 against 1326.
Both overruns come from #16535 replacing inline fully qualified names with
imports, which adds import lines while the usage lines keep their count — 4
imports in RuntimeBrokerService and 1 in RuntimeBindingProvisioning — without
updating the ceilings that #16501 had just recorded after the extraction.
The SDK Java lane runs the merge ref, so every PR that merges main inherits
the failure.

The ratchet only lets ceilings go down, so reclaim the lines instead of
raising the pins: join expressions that were already split across two lines.
RuntimeBindingProvisioning's single Objects.equals operand, and the four
BrokerValues.requireId assignments in release() and releaseMount(). Every
joined line is at most 88 characters; the diff is formatting only and keeps
#16535's import style.

ClassSizeRatchetTest red before / green after; RuntimeBrokerServiceTest 199
green; runtime-broker checkstyle and spotbugs clean. No pin was edited.
2026-11-07 22:24:43 +08:00
易良andyiliang114 708ba347d3 feat(ci): add disposable external PR runners (#16513)
* feat(ci): add disposable external PR runners

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

---------

Co-authored-by: yiliang114 <[email protected]>
2026-11-07 14:24:33 +00:00
d85e911c78 fix(cli): restore OpenTUI dialog editing controls (#15908)
* fix(cli): restore OpenTUI dialog editing controls

* fix(cli): preserve dialog Ctrl+C editing ownership

* fix(cli): clear OpenTUI Source fields on Ctrl+C

* fix(cli): flatten pasted breaks and read live focus in OpenTUI dialogs

* test(cli): pin column-exact dialog rows and the second-Ctrl+C exit

* test(cli): correct the row-oracle comment to the measured geometry

* test(cli): name the measured phantom-column shape in the Sources oracle

---------

Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-11-07 13:57:51 +00:00
Shaojin WenandQwen-Coder cf29f90062 fix(ci): route the review/resolve permission gate to the ECS pool (#16482)
* fix(ci): route the review/resolve permission gate to the ECS pool

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

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

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

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

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

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

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

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

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

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

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

Two batches, verified together before push:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-07 13:47:42 +00:00
Shaojin WenandQwen-Coder 47d41dbf71 fix(channels): keep the cursor write alive when the temp file mode cannot be tightened (#16257)
* fix(channels): keep the cursor write alive when the temp file mode cannot be tightened

Mounts without Unix permissions (SMB/CIFS, exFAT, some FUSE) accept the
write but refuse chmod, so the unguarded temp-file chmod made every save
throw before the rename and the cursor was never persisted. Guard it like
the directory chmod and pin the behaviour with an EPERM test.

Also seed the scope directory at 0o755 in the permissions test so it
proves the tightening, and pin that an empty scoped cursor starts fresh
instead of falling back to the legacy global file.

* test(channels): assert the injected chmod fault fired in the cursor guard test

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-07 13:46:41 +00:00
Shaojin Wen 4c8ebeab4d feat(core): view one pull request by URL for the PR-state watch (#16530)
* feat(core): view one pull request by URL for the PR-state watch

Add fetchGitHubPullRequestView(url, cwd, env?), the single-PR view the
daemon's PR-state watch will use (step two of PR A in
docs/design/2026-11-04-daemon-pr-state-transitions.md, section 1). It
runs `gh pr view <url> --json url,title,headRefOid,mergeable,state,
statusCheckRollup` and returns the canonical URL, title, head SHA,
open/merged/closed, mergeable (absent while GitHub reports UNKNOWN), the
CI state and the failing check names (`<workflow> / <job>`, the check
name or the status context; once each, at most 20).

Only an http(s) /owner/repo/pull/<n> URL is accepted, and it is rebuilt
without credentials, query, fragment or tab path before it reaches gh,
so an issue URL, /PULL/7 or /pull/0 never becomes a branch lookup. The
working directory must exist, so a missing one is not reported as gh
being unavailable. Failures carry a reason (not_a_pr_url, cwd_missing,
gh_failed, timed_out, unparsable). There is no host check; the caller
restricts URLs to the allowed GitHub hosts.

aggregateChecks is exported with unchanged behaviour; its failing-entry
predicate is shared with the failing-name extraction. Every rollup entry
still counts: gh's JSON exports no event, app or workflow id, and a
dedupe by names and times would let a skipped or other-trigger run hide
a current failure.

The design doc's Files-affected row for core names the real signature
and fields.

* fix(core): report gh's diagnostic, not Node's argv, when gh fails

execFile's callback error has no stderr property (only the promisified
form attaches it), so ghErrorMessage fell back to Node's "Command failed:
<argv>" message: a failed PR list, view or create reported the gh command
line (for create, the PR title and body) ahead of gh's diagnostic, which
the 4,096-character raw cap and the route's 512-character display cap
could then cut off. The three callback wrappers now attach the
callback's stderr, as the PR issues wrapper already does.

When gh exits or is signalled with nothing on stderr, the message names
the exit code or the signal instead of falling back to the argv, and a
failure Node raises itself (a string code such as a spawn error or an
overflowing buffer) keeps Node's message even if gh wrote something.
`gh api graphql` failures of the PR issues fetch, which share the
helper, get the same treatment.

The test helper now fails the way execFile does: an exit 1 unless told
otherwise, with stderr only as the callback's third argument, so the
stderr tests exercise the wrappers and the stderr-over-exit-code order
rather than the mock. Tests cover a silent exit, a signal, a buffer
overflow with stderr, and a silent create that must not echo the title
and body; the view's failing-names test gains a failing check run with
an empty name and a status context with an empty context, which fail the
head but are not named.
2026-11-07 21:33:43 +08:00
9d1cf396a5 fix(web-shell): fall back to English copy when the UI language has no relay dictionary (#15474)
* fix(web-shell): fall back to English copy when the UI language has no relay dictionary

* test(web-shell): point the relay fallback case at an unshipped locale and pin the translator fallback

The relay regression case used 'ru', which #13685 shipped with its own relay entry, so the '?? COPY.en' arm never executed and the guard was unpinned (mutation matrix: M1/M2/M3 survived). The case now uses 'fr', a locale outside the shipped languages union, which kills the fallback mutants. The whole-shell translator gets the same treatment: 'getTranslator' falls back to the EN table for an unshipped locale, and a new i18n case pins it.

---------

Co-authored-by: Сергей Панченко <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-11-07 13:02:33 +00:00
Shaojin Wen c1aa266e82 perf(core): compare a grown local journal prefix in chunks (#16507)
- Re-read a file that grew during the read through one 64 KiB chunk
  instead of a second buffer of the whole size, so an unbounded read of a
  transcript being appended to holds one copy of it.
- Name the same-length rewrite within one timestamp tick as a cost
  decision in the comment.
- Inject the read races through one helper, and cover a same-length
  rewrite with moved timestamps, a bound below the file size, a shrink that
  stays above the bound, a short comparison read and a comparison over
  several chunks. Drop two cases that duplicated others.
2026-11-07 20:56:23 +08:00
4d61487419 ci(release): only file issues on failure and reconcile stale ones (#15852)
* ci(release): file failure issues only for failures and close stale ones

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

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

Fixes #15840

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

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

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

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

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

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

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

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

---------

Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: 2SinceJuly <[email protected]>
2026-11-07 12:52:38 +00:00
ChiGao 978d9a23c3 fix(web-shell): respect optional transport capabilities and local turn timing (#16536) 2026-11-07 20:51:17 +08:00
Shaojin Wen 0807f5d391 refactor(channels): share the bound-session prompt contract via channel-base (#16523)
* refactor(channels): share the bound-session prompt contract via channel-base

Move the GitHub channel's operator instruction template contract (key
validation against a caller-supplied event vocabulary, template
validation, the role/event/lane resolution chain, the %var% renderer)
and the bound prompt envelope (platform header, untrusted-data and
not-published notices, labelled trusted-instruction block, sanitised
data lines, Role line, visible 6,000-code-point body cap) into
@qwen-code/channel-base, so the daemon's PR-state watch can reuse them
instead of keeping a copy (step one of PR A in
docs/design/2026-11-04-daemon-pr-state-transitions.md).

The adapter keeps its vocabulary, the deliverToBoundSessions
precondition, the comment-lane class, metadata parsing and provenance
line, and its output is unchanged except for two rendering fixes:
%constructor%-style names now stay literal like any other unknown name
(they rendered Object.prototype members), and every substituted value is
sanitised, as the templates design specified (only %title% and %sender%
were; those two are still also capped at 200 code points).

Tests pin the legacy, role-aware comment and role-aware watch prompts
byte for byte (strings captured from the previous adapter), %url% on a
GitHub Enterprise origin with a port, and the exact validation messages.
The design docs, the template editor design and the user guide are
updated to match.

* fix(channels): keep the trusted instruction label inside the tag peel window

The bound prompt labelled its trusted block `[Operator instruction —
trusted, from <source>]`. With the channel's source that label's content
is 65 characters, one over the 64 that sanitizePromptText peels from a
start-of-line tag, so an event body that began with a copy of the label
kept its brackets and read as a second trusted block. The label is now a
fixed `[Operator instruction — trusted]` with the source on a `Source:`
line under it, so it stays peelable whatever source a producer names.
The platform the header names becomes a closed list
(BOUND_EVENT_PLATFORMS) for the same reason: the header's content is the
name plus 36 characters. A test runs over that list and feeds a copy of
the header and of the label through the body and a data line; another
puts a copy at the start of a substituted value.

Also from review: pin the untyped-winner guard of the resolution chain,
empty values rendering as empty, and misspelt `watch:` types being
rejected, and align the templates design's validation plan with the cap
that applies only to %title% and %sender%.

The templates design, the daemon design and the user guide show the new
label; the templates design's prompt example gains the not-published
notice it was missing, and the daemon design's validation plan now
expects only the prompt without a template to stay byte-identical.
2026-11-07 12:33:10 +00:00
Shaojin Wen ced03aef5d feat(serve): reclaim idle channel sessions (#16532)
Channel workers keep an SSE event pump attached to every cached route,
so the idle reaper treated those sessions as actively watched and never
reclaimed them. A workspace could fill every maxSessions slot with
sessions nobody was using and then reject new creates with 503.

Let the reaper look past a channel-owned subscriber while preserving the
local-work, active-prompt, and child conditional-close guards.

Surface session_limit_exceeded as a banner with a release action. The
release dialog falls back to the workspace daemon client when it opens
from the empty new-session screen, where no session client exists.

Design: docs/design/channel-session-idle-reclaim.md
2026-11-07 19:42:38 +08:00
Shaojin WenandQwen-Coder 61aef512e7 feat(web-shell): offer a details side task from pending AskUser questions (#16458)
* feat(web-shell): offer a details side task from pending AskUser questions

While an AskUserQuestion is pending, the Web Shell hides the composer and
the btw shortcut ignores the pending-approval state, so a user who does not
understand the options can only answer blindly or ignore the question.

Add an "Ask for details" action to the pending panel. It opens a side task
whose prefilled prompt snapshots the question and options and asks for a
verified explanation — background, per-option meaning, differences, impact
and risks, and a recommendation — delivered as a self-contained single-file
HTML in controlled ASD-STE100-style sentences under .qwen/tmp/, previewable
in the right panel. The side task inherits the parent context but never
answers the question or touches the main session; the question stays
pending until the user answers or ignores it.

The action is main-view-only and gated on the session_side_task capability,
matching /btw side. No daemon, protocol, or settings changes.

* fix(web-shell): one details side task per question, reachable on narrow panels

Post-publication audit of the AskUser details action found three issues:

- Repeat clicks provisioned duplicate side tasks for the same pending
  question, each burning a model turn. Give the details tab a stable id
  derived from the parent session and the permission request id and upsert
  it, reusing the artifact panel's open-or-focus pattern; /btw side keeps
  its unique per-invocation draft id.
- The question snapshot dropped multiSelect, so the explanation prompt did
  not say options can combine. Append a [multiSelect] marker (strict true).
- On a 320px two-question panel the actions row overflowed to the left and
  pushed the details button out of the panel (left: -103px in a real-browser
  probe; scrollWidth misses left overflow on a flex-end row). Let the row
  wrap and pin a narrow-viewport regression test asserting every action
  button stays inside the panel and stays clickable.

Each fix carries a red/green witness: the repeat-click App test, the
multiSelect unit test, and a flex-wrap mutation of the narrow visuals test.

* fix(web-shell): keep the details tab id within the daemon clientId budget

The dedup fix derived the details tab id from the parent session id AND the
request id. SideTaskPanel derives the pane's daemon clientId as
side-task:<parentSessionId>:<tabId>, which with two uuids reached 138 chars —
over the daemon's MAX_CLIENT_ID_LENGTH (128). The child session load then
400s, the pane never connects, and the prefilled prompt is never sent.
Caught by re-running the real-daemon scenario on the post-fix head; mocked
unit tests could not see it.

Derive the stable id from the permission request id alone (a uuid; tabs are
already bucketed per parent session), bringing the clientId to 101 chars.
Add a SideTaskPanel regression test asserting the derived clientId stays
within 128 for uuid-length inputs; the old id shape fails it with 138.

* fix(web-shell): retain the details question-to-child association across tab close

Review found that deduplicating only against open tabs loses the existing
child when the details tab closes: clicking "Ask for details" again for the
same pending request provisioned a second child, and reopening the child
from the side-task catalog first did not deduplicate either (its tab id is
side-task:<sessionId>, not the details-request id).

Persist the pending-request-to-child association per parent session in
localStorage, written when the details child is created. On click, focus
whichever open tab shows the known child (details tab or catalog tab);
with no tab open, reopen one attached to the existing child session — no
draft, no initial prompt, so nothing is provisioned or sent again. One
entry per parent supersedes the resolved question's association, keeping
the store bounded; the association also survives reloads.

Regression tests cover both reported paths (close + re-click keeps exactly
one createSideTaskSession call and reopens with the child's sessionId and
no initialPrompt; catalog reopen + click focuses the single existing tab).
Red witness: dropping the association lookup fails the close + re-click
test with two creation calls.

* fix(web-shell): close the details association lifecycle gaps

Re-review of the association persistence found three lifecycle gaps, all
fixed here with regression tests and mutation witnesses:

- A child exists before its explanation prompt is admitted. Reopening a
  closed details tab now retains the prompt on the tab; the pane's existing
  empty-session guard delivers it exactly once (only while the child has no
  user prompt) and never re-sends it.
- Archiving a details child left its persisted association pointing at an
  unloadable session (the daemon's load route 409s session_archived).
  archiveSideTasks now invalidates associations pointing at archived
  children, so the next click provisions a fresh child.
- Closing the draft tab before creation resolved skipped the association
  write, orphaning the child and provisioning a duplicate on the next click.
  The parent identity is captured when creation starts, and the write no
  longer depends on the tab still existing.

Also pin two invariants the deep-verification report found unasserted:
the derived clientId budget with uuid-length inputs through the real tab-id
derivation (App test), and the formatter's array guard against non-array
objects (formatter test).

* fix(web-shell): supply the unsent details prompt to a catalog-reopened tab

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

* fix(web-shell): treat media-only user prompts as interaction in the side-task guard

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

* fix(web-shell): validate details child archive state at the read site and address review suggestions

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

* fix(web-shell): replace stale details tab on fresh draft and re-check pending after the liveness query

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

* fix(web-shell): replace a details tab bound to a different child than the validated binding

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-07 19:16:53 +08:00
Shaojin WenandQwen-Coder 5b4dcbad19 fix(channels): watch every open bound PR and keep polling during public turns (#16502)
* fix(channels): watch every open bound PR and keep polling during public turns

The PR state watch selected bound PRs purely by binding recency under a
hard cap of 30, so older open bindings were never polled once newer
bindings (including already-merged ones) filled the budget, and merged
bindings outside the cap never received the terminal observation that
would retire them. Selection now watches open PRs first and drains
terminal-pending bindings with the leftover budget; the cap becomes the
configurable prWatchLimit (default 30, range 1-500, fractional values
round up), exposed in the channel descriptor and management API.

Public-path tasks (review requests and assignments on unbound PRs) ran
their full agent turn inside the poll loop, freezing watch detection,
bound delivery, and every other notification for the turn's duration
(observed: 64 minutes on a production daemon). They now execute in a
background FIFO runner (concurrency 1); claims stay durable and
synchronous, the cursor gate and crash recovery keep their semantics,
and bound/watch deliveries stay inline. The first-contact body feed
gains a thread-level gate so an async body turn cannot overtake and
steer-cancel a still-retrying comment turn.

* fix(channels): close review gaps in watch coverage and the async task runner

Review round on the bound-delivery reliability fix. Three correctness
findings, each reproduced against the base:

- The first-contact body deferral gate could lose the body permanently:
  nothing reserves a deferred feed, and the end-of-poll cursor commit can
  mark the thread read once the blocking task settles. Remove the gate;
  the runner FIFO already serializes the body behind the comment turn.
  The absence gate now runs synchronously at dispatch so backpressure
  deferrals keep the false verdict the lane's body suppression needs.
- disconnect retired the runner generation but the winding-down runner's
  restart hook drained the queued tasks anyway. Guard the restart by
  generation and resolve idle waiters at disconnect.
- Recovery could re-resolve a backpressure-parked task whose public turn
  was running (the running transition merges patches) and deliver it
  bound a second time. Skip in-flight ids before any recovery branch
  mutates a task; crashed tasks without an in-flight entry still recover.

Also: gone-session public fallbacks requeue through the serial runner
instead of awaiting the turn in the poll lane (head-of-line block and
steer-cancel on shared sessions); a same-instance reconnect keeps the
orphaned runner's executing id in flight so recovery cannot double-run
it; held threads skip redundant comment re-fetches via a poll-start
snapshot; escaped runner failures are journaled once per task; the watch
budget reserves a terminal slice (20%, at least one) so the drain
progresses even when open PRs fill the limit; stale-open bindings tier
as terminal once the watch observed closed; the limit constants are
exported for the descriptor, the field is registered and translated in
the Web Shell editor, and validation enforces the bound-delivery
prerequisite; the rate arithmetic is documented at every surface.

* fix(channels): drain terminal bindings outside the open watch budget

The in-budget terminal reservation excluded up to ceil(limit*0.2) open bindings while a merged/closed backlog drained, permanently swallowing edge events on those bindings — the defect this branch fixes. Open PRs now take the full prWatchLimit; terminal-pending bindings drain in an additional slice outside it (20%, at least one, plus leftover budget), bounded at limit + ceil(limit*0.2) pulls.get per poll during the drain.

Pins the verification fixture (30 open + 5 newer terminal at limit 30: oldest open observed, its head change delivered) and keeps the removed in-budget variant as a mutation witness. Rate arithmetic updated on the descriptor, i18n (EN/ZH), user guide, and bilingual design docs.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-07 19:16:20 +08:00
jinyeandClaude Opus 5.5 c2e77d1473 refactor(contracts): import inline fully qualified names in contracts (#16534)
Replace 183 inline fully qualified type names across 56 Contracts files
with ordinary imports, using the same skip rules as the Runtime batch.
The compiled classes are unchanged: all 604 classes are identical under
javap -c -p ignoring constant-pool indices.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-07 18:43:31 +08:00
1762efe4cb fix(channels): keep a delivered turn completed when the notice send fails (#16512)
* fix(channels): keep a delivered turn completed when the notice send fails

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

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

Refs #16441

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

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

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

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

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

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

---------

Co-authored-by: zzj <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-07 10:40:37 +00:00
jinye 1b9aef2264 refactor(adapters): import inline fully qualified names in adapters (#16533) 2026-11-07 18:36:04 +08:00
jinye 157664acb2 refactor(runtime-broker): import inline fully qualified names (#16535) 2026-11-07 18:09:32 +08:00
4c5820c745 fix(web-shell): close out managed-host review threads from #15928 (#16203)
* fix(web-shell): close out managed-host review threads

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

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

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

Every new assertion was verified red with the fix removed.

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

Three review follow-ups from the #15928 threads:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

Review round 2 on #16352.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-07 17:43:51 +08:00
jianyx1 222ca313e2 test(ci): harden TUI workspace reconciliation (#16033) 2026-11-07 17:42:14 +08:00
jinyeandClaude Opus 5.5 0acd2851cd refactor(runtime-broker): extract binding provisioning from RuntimeBrokerService (#16501)
* refactor(runtime-broker): extract binding provisioning from RuntimeBrokerService

RuntimeBrokerService still carried the whole Runtime binding lifecycle:
durable provisioning and attestation, the bounded reconciliation loop,
lost-binding cleanup and the trusted maintenance recovery path. Those
members only depend on each other and on the operation claim, so they
move into a package-private RuntimeBindingProvisioning collaborator,
alongside the existing dispatcher, reconciler and recovery classes.

This is a pure move. The service delegates binding acquisition, recovery,
the deadline and step-timeout helpers and attestation checks to the new
class. The provision failure handler is now owned by the collaborator,
which removes the service's constructor-time method reference cycle.
BindingRenewal stays in the service because the drain path also uses it;
the collaborator receives its constructor as a factory.

RuntimeBrokerService shrinks from 4174 to 2948 lines.

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

* fix(runtime-broker): tighten class-size ratchet after binding extraction

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-07 17:01:35 +08:00
易良andQwen-Coder 5faa620af9 fix(core): keep the user's instruction last in the model request (#16274)
* fix(core): keep the user's instruction last in the model request

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

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

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

Refs #16259

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-07 08:31:15 +00:00
Job NatdhanaiandClaude Opus 5.5 db9478db8b fix(core): keep follow-up suggestions in Thai, Lao, Khmer and Myanmar (#16341)
* fix(core): keep follow-up suggestions in Thai, Lao, Khmer and Myanmar

getFilterReason() counted words by splitting on whitespace and exempted
only CJK, so a space-free Thai, Lao, Khmer or Myanmar suggestion was one
"word" and was dropped as too_few_words. Count word-like segments with
Intl.Segmenter for these scripts (2 characters minimum, 12 words maximum,
matching the prompt). The segmenter is created lazily and only on a
full-ICU Node build, because on small-icu builds segment() can crash the
process (#11747) and this filter also runs outside the CLI's ICU probe;
without it a 60-character bound applies. Adds the first CJK and
Southeast Asian script tests for the filter.

Fixes #16339

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

* test(core): pin the segmenter path for Thai follow-up suggestions

Add two cases where the word count and the 60-character fallback
disagree (13 words in 37 characters; 11 words in 76 characters), so the
tests fail if the segmenter is silently disabled.

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

* fix(core): keep whitespace bounds for spaced Thai follow-up suggestions

Use the segmenter only for suggestions without spaces. A Thai suggestion
that also names a path or a command (e.g. "รัน npx vitest run ...") has
few whitespace words but many segments, so it was dropped as
too_many_words although the base branch accepted it.

Also test that no segmenter is created when process.config reports a
small-icu build.

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

* fix(core): keep the slash exemption for Thai follow-up suggestions

A /-prefixed suggestion in Thai, Lao, Khmer or Myanmar has one
whitespace word, so it entered the segmenter branch and could be
dropped as too_many_words before reaching the slash exemption. Return
early for it, as the space-delimited branch does.

Update the documented filter rules: these scripts are counted by word
segmentation, and a single word in them is allowed.

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-07 08:30:22 +00:00
chencheng (云谦)andShaojin Wen 29fa7bbe06 chore(types): compile cleanly under the TypeScript 7 native compiler (#16463)
* fix(types): compile cleanly under the TypeScript 7 native compiler

tsgo (@typescript/native-preview) reported 48 errors across the
typecheck projects while tsc 5.8 reported none. This removes all of
them without changing what tsc emits, so a tsgo typecheck lane or a
later compiler switch can start from a clean tree.

- Drop `baseUrl` from five tsconfigs. TypeScript 7 removed it, and
  every `paths` target here is already relative to its tsconfig.
- Set `rootDir` where TypeScript 7 no longer infers it (web-shell
  declarations, sdk-typescript), and list node types explicitly for
  cua-driver now that @types are no longer included by default.
- Annotate Buffers built by Node's Buffer factories as
  `Buffer<ArrayBuffer>` (or widen to `Buffer` where a variable later
  receives stream chunks); the ES2024 lib no longer treats
  ArrayBufferLike as an ArrayBuffer, so these failed at fetch, Blob,
  and Response call sites. The two call sites that take caller- or
  interface-supplied bytes assert `BodyInit`, matching the existing
  XMLHttpRequest upload path.
- Remove three redundant `?? []` / `?? undefined` operands that the
  newer checker reports as unreachable.
- Declare `*.css` modules for the two integrations that import
  stylesheets, now that side-effect imports are checked.

* fix(types): include web-shell client declarations in the build config

The config's include listed only client/index.tsx, so client/vite-env.d.ts
and client/local-files/file-system-access.d.ts never entered the program.
Removing baseUrl unmasked 227 tsgo errors (and 222 under tsc 5.8) for the
CSS module and File System Access declarations they provide.

---------

Co-authored-by: Shaojin Wen <[email protected]>
2026-11-07 08:23:16 +00:00
Shaojin Wen f2e9a3fc66 feat(channels): add GitHub repository event filter with allowlist and blocklist (#16493)
* feat(channels): add GitHub repository event filter with allowlist and blocklist

The GitHub channel processed notifications from every repository the
authenticated account can see. The only repository-level control was the
group access allowlist, which cannot express "everything except these
repositories" and hides a plain event filter behind access-policy
machinery.

Add repoFilterMode (off / allowlist / blocklist) and repoFilter to the
GitHub plugin. The filter runs at notification intake, before the reason
filter, journals capped repo_filter skip rows plus one collapsed count
row, and never holds the poll cursor. One shared validator rejects an
active mode with an empty or malformed list at save time and at connect
time; while the mode is off a stored list is ignored, so it can be parked
without blocking unrelated saves. Notifications about bound PRs are
filtered too; only their PR state watch events bypass the filter.

The Web Shell editor gains a Repository Filter select and a Filtered
Repositories list shown only while a mode is active, with the visibility
and validation gated on the same predicate. Pasted repository or PR URLs
are shortened to owner/repo on save; non-repository GitHub URLs are
rejected instead of silently shortened. A parity table binds the editor
mirror to the plugin's own validator, and the visuals suite captures the
new editor in both themes.

* fix(channels): close review findings on the repository event filter

Address the verify-round findings and the triage review notes on #16493:

- F1 (Medium): the reserved-path guard missed sibling GitHub areas, so
  pasted URLs like github.com/apps/renovate or /features/copilot were still
  silently shortened to nonexistent repositories that both validators
  accepted — fatal under an allowlist. Extend the guard with the measured
  missing roots plus the other known two-segment areas, export the set, and
  replace the sampled fixture with an enumerated test over every member plus
  explicit regression pins for the reported shapes. Removing a listed root
  now fails the pins; the denylist's best-effort nature is documented instead
  of overclaimed.
- F2 (Low): pin the documented ordering — a notification excluded by both
  gates is journaled repo_filter, not reason_filter. The verify round's M7
  mutation (swapping the two blocks) survived the old suite and now fails
  this test.
- Triage boundary note: pin that a bound PR's state watch events bypass the
  repository filter, so a future refactor of the watch call site cannot
  silently silence bound sessions.
- Docs: soften the URL-rejection claim to "known non-repository areas" in the
  user guide and both design docs, and state the residual denylist risk.

* fix(channels): close the ten inline review findings on the repository filter

- R1-1: close the arbitrary-host class structurally. The paste-shortener now
  only shortens URLs on the channel's own GitHub host (github.com plus the
  GHES host derived from baseUrl, api. prefix stripped); api.github.com,
  gist.github.com, gitlab.com and any other host pass through for validation
  to reject. Add the missing advisories/copilot areas, a snapshot assertion
  over the whole reserved set (removing any member now fails, which the
  enumeration alone could not witness), and foreign-host fixtures.
- R1-2: seed paginate before connect() in the rejection helper so a
  successful connect no longer prints an unasserted poll-error backoff line.
- R1-3: dialog case opening on a stored active mode — the list shows the
  stored text and a re-save keeps both keys.
- R1-4: pin the notification side of case-insensitive matching (mixed-case
  subject URL vs lowercase entry) and the normalizer's trim/empty handling.
- R1-5: pin the complement of the watch bypass — a notification about a
  bound PR in a blocklisted repository is filtered, not delivered.
- R1-6: capitalized reserved-root variant pins the guard's own lowercasing.
- R1-7: vocabulary test binding the repoFilterMode descriptor options and
  default to the exported REPO_FILTER_MODES, mirroring prWatchEvents.
- R1-9: two-poll journal budget reset test (42 rows, two counted rows).
- R1-10: extend the Off-retirement case with the discriminating switch back
  to Allowlist, where a stale error would remount.
- R1-11: typing a valid entry (or correcting a malformed one) now retires
  the shown instruction through refreshErrors, with two no-save dialog
  cases; Submit remains the only error population site.

Mutation witnesses on this head: dropping chatId lowercasing, the host
gate, the list-typing retirement branch, and the refreshErrors guard each
fail exactly their new test; all restored byte-identical.

* fix(channels): exclude the channel's own API host and paths from URL shortening

Round-3 review findings on #16493, all closed in this commit:

- R1-1 (Critical, fix-induced): the host gate re-admitted the channel's own
  API host — with baseUrl set, a pasted REST URL shortened to `repos/octo`
  (api.github.com) or `api/v3` (GHES /api/v3), phantom repositories both
  validators accepted. Only the WEB host now qualifies: an api.-prefixed
  baseUrl host contributes only its stripped form, and paths under the
  baseUrl's own API prefix are excluded. Genuine GHES web URLs still
  shorten. Pinned with baseUrl actually passed through validation, for both
  REST shapes; re-admitting the raw API host reddens the test.
- R2-1: the baseUrl argument now has a call-site witness on both paths —
  a GHES draft validates clean and saves ['octo/repo'] through
  buildChannelUpsertRequest, mirrored in the parity table against the
  daemon authority. Dropping either argument reddens its half.
- Deferred notes (all three closed, none deferred onward): trailing
  sentence punctuation is stripped before matching (a pasted
  `…/octo/repo.` no longer becomes the phantom `octo/repo.`);
  `enterprises` joins the reserved set (snapshot updated); and a dialog
  case witnesses the parked-list round trip — hidden while Off, restored
  with its text on re-enable.

Mutation witnesses: re-admitting the raw API host, dropping the validator
baseUrl argument, and dropping the save-path baseUrl argument each fail
exactly their new test; all restored byte-identical.
2026-11-07 16:16:22 +08:00
samuelhsinandyungsen.hys 752e399b3b feat(web-shell): surface task Q&A activity above the composer (#16037)
* feat(web-shell): add a waiting minigame to the composer

Show a dismissible horizontal-Tetris strip above the main composer while a
prompt is streaming. The game closes when the turn ends, ignores editor
keyboard targets, and stays out of split panes and coarse-pointer devices.

Add engine and component tests, EN/ZH copy, design docs with page
screenshots, and a Chromium E2E scenario.

Refs #16036

* docs(web-shell): point the waiting minigame design at the committed E2E spec

* fix(web-shell): move the waiting minigame trigger into the status line

Replace the full-width waiting strip with a compact gamepad button at the
end of the streaming status row. The main composer now owns the open
state, and the board mounts above the status row only while open.

Keep the game's window keydown listener stable through a ref so rerenders
cannot drop the Escape that collapses the board, and update the component
tests, E2E scenario, screenshots, and both design docs.

* fix(web-shell): keep the waiting minigame scoped and recoverable

Pause and resume correctly after the tab is hidden, scope game keys to
the focused card so Escape no longer arms stream cancellation, let
focused buttons keep Space, restore the sound toggle, and keep the
board grid out of the aria-hidden wrapper that also held the overlays.

* fix(web-shell): keep the waiting minigame recoverable and scoped

Resume gravity after the tab becomes visible again, create the audio
context when sound is enabled, let the card's own buttons keep
Space/Enter, leave Escape to open dialogs, and keep the restart button
out of the aria-hidden board. The main composer now lets the open game
own Escape so the first press collapses it without arming stream cancel.

* feat(web-shell): add a waiting card with activity, usage and games

Open a waiting card from the streaming status button. The card starts on the
task's 44-bucket Q&A activity heatmap, switches to token usage instead of a
credits balance, and places a Qwen mark that opens a Tetris/Snake picker.
Games share the 44x7 grid and offer back-to-activity and switch-game actions.

Add the activity and Snake engines, component and E2E coverage, a local
harness with sample data, and design docs with page screenshots.

Refs #16036

* feat(web-shell): refine waiting card activity and game intros

Add hover details and click-to-locate to the activity heatmap, start a
random game from a delayed cold-cell Qwen entry, and play a QWEN CODE
intro that flies the blocks into the Tetris start cluster. Snake keeps
its intro static, eats an apple emoji, and starts at a slower speed.

* feat(web-shell): show task activity in a daily calendar

Replace the task-span buckets with a fixed 44-week day calendar, including
month labels, per-day hover details, click-to-locate and a stable card frame
across activity, usage, intro and game views. Gate the Qwen game entry behind
the waiting games configuration so the activity card stays the default
experience.

* fix(web-shell): show a gamepad icon on the configured game entry

Replace the Qwen brand mark in the calendar's game entry with a gamepad
icon, remove the now-unused Qwen logo component, and let the local harness
read the waiting games config so ?waitingGames=1 opens the preview with the
entry visible. Game screenshots and docs now describe the gamepad mark.

* fix(web-shell): read cached tokens from the connection fallback

The usage card falls back to the connection's token usage before any
assistant usage frame arrives, but the fallback read the message-level
cachedTokens field while the connection object carries cachedReadTokens.
Accept both names and pin the connection shape in the unit test.

* fix(web-shell): keep the game entry hit area inside its cell

Give the calendar game entry a positioning context so its forgiving hit
area stays bounded when prefers-reduced-motion disables the intro
animation and removes the transform that used to establish it. Cover the
reduced-motion path with an E2E that hovers and clicks a day while the
entry is visible.

Also reuse one Intl formatter pair per locale, format task token totals
above 1M as M, and drop the unused no-activity copy.

---------

Co-authored-by: yungsen.hys <[email protected]>
2026-11-07 15:32:23 +08:00
Shaojin WenandQwen-Coder f387c3a75a fix(ci): recreate git metadata before the publish-triage checkout (#16494)
* docs(ci): state the real publish-triage safety argument

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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


Patrol-Run: qwen-issue-patrol/jmw4xgcu3zh

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Addresses the two Criticals from the #16432 human review.

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

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

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

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

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

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

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

Refs #16432

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

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

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

---------

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

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

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

Fixes #16301

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-07 10:42:48 +08:00
791989cdb8 fix(vscode): keep sibling model settings when writing provider config (#15829)
* fix(vscode): keep sibling model settings when writing provider config

Configuring a Coding Plan or a model provider from the panel replaced the
whole settings.model object with { name }, silently dropping keys the CLI
owns: baseUrl, reasoningEffort, maxSessionTurns and the run budgets.

Write name and its paired baseUrl key by key instead, matching the other
sections in the same writer and the CLI's own writers. The baseUrl is
written rather than merely preserved because the resolver's merge invariant
needs a present value (or an empty-string tombstone) to override a stale
one in a lower-priority scope.

Fixes #15822

* test(vscode-companion): pin region and custom-endpoint baseUrl writes

The api-key fixtures reused the fallback literal, so a regression that
dropped the modelProviders lookup kept the suite green; likewise only the
china branch of the Coding Plan model.baseUrl write was pinned. Seed a
custom gateway URL plus an active-model-absent fallback case, and assert
the global region writes its own endpoint.

* test(vscode-companion): pin the api-key install path's model baseUrl

* test(vscode): pin the empty baseUrl tombstone for id-only installs

Subscription install plans select a model by id alone, so the only thing
clearing a stale model.baseUrl is the empty-string tombstone. The existing
install-path cases always pass a baseUrl, leaving the serialization half of
that branch unasserted on disk.

* fix(vscode): take the OpenAI default base URL from the core helper

The active-model write re-spelled https://api.openai.com/v1 while the paired
provider entry and the login flow both go through
getDefaultBaseUrlForProtocol. Moving that default in core would then leave
this path behind, so call the helper instead.

---------

Co-authored-by: 肖洪进 <[email protected]>
Co-authored-by: 2SinceJuly <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-11-07 02:34:22 +00:00
Shaojin Wen 930962ab51 fix(managed-agent): let a late snapshot publication wait for the claim instead of losing deadlocks (#16489)
* fix(managed-agent): let a late snapshot publication wait for the claim instead of losing deadlocks

Since #16143 resource_key is a secondary unique index, so a second
publication's manifest upsert queues for the first's uncommitted row with
a next-key lock. Every page the first Build inserts into that lock's gap
rolls the queued publication back, and on longer journals #16446's three
attempts run out: on a 31-page journal a late second publication failed
for 15 of 40 snapshot ids.

The claim now locks the manifest row by key first. At READ COMMITTED that
takes a record lock when the row exists, which does not block the inserts
into the gap, and only a missing row is upserted. Two publications that
both find no row can still meet in the upsert; #16446's retry handles the
one rolled back, whose locking read then waits for the winner.

The MySQL cases pin both paths and assert the key order they depend on.

* test(managed-agent): pin the snapshot claim's reads, upserts and rollback interleaving

- State in the claim's Javadoc that the wait on the manifest row lasts at
  most innodb_lock_wait_timeout and that a lock wait timeout (1205) is not
  retried, only deadlock victims (1213) are.
- Count the claim's locking reads and upserts in the test JdbcTemplate and
  assert them in the new concurrent cases, so a reworded claim statement
  fails those cases instead of turning them into plain races.
- Cover a publication queued on a claim that rolls back: it finds no row
  and publishes the snapshot itself.
- Check that publications run at READ COMMITTED, which the claim's comment
  relies on.
- Read the monitor fixture through one helper.
2026-11-07 09:58:05 +08:00
jinyeandClaude Opus 5.5 2258518d46 refactor(runtime): import inline fully qualified names in runtime modules (#16504)
The runtime provisioner, transport and host modules spelled 80 type
references as inline fully qualified names. They now use imports.

The conversion only touches code (never strings or comments) and leaves a
name qualified whenever importing it could change what any name in the
file resolves to: an existing or same-package type with that simple name,
a declared or nested type, java.lang, a bare identifier already in use, or
a wildcard import.

The compiled bytecode is unchanged: javap -c -p of all 49 classes in the
seven modules is identical before and after, ignoring constant-pool
indices.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-07 09:42:14 +08:00
a84493d628 fix(core): report a cancelled shell as interrupted, not completed (#15836)
* fix(core): report a cancelled shell as interrupted, not completed

The shell tool kills the process group on a user cancel but returns an
ordinary result, so the scheduler's post-execute cancellation branch labelled
the call "already completed" and told the model not to retry work that never
finished. Let producers mark that outcome on the result; the shell tool sets it
on a non-timeout, non-promote-refused abort.

Closes #15833

* fix(core): keep a cancelled shell's partial output with the notice

The scheduler replaced the whole model-visible payload with the cancellation
notice, so the output a killed command had already produced disappeared even
though the shell tool reports it. Append the producer's own interrupted text
when it sets interruptedBeforeCompletion, and pin the marker's negative cases
that the existing suites did not distinguish.

* fix(core): flag returned cancellations as interrupted

The monitor, create_sub_session, ask_user_question and workflow tools
report a cancelled call by returning a result instead of throwing, so
the scheduler's returned-means-finished assumption told the model the
work had completed. They now set interruptedBeforeCompletion, which the
scheduler already honours for the shell tool.

* fix(core): mark only aborted unanswered questions as interrupted

A plain decline is a completed decision. Setting the marker on it is inert
today, but would report a finished interaction as "do not retry" if a future
read site consumed it.

* fix(cli): report an interrupted shell as interrupted in ACP sessions

---------

Co-authored-by: 2SinceJuly <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-11-07 01:17:33 +00:00
jinyeandClaude Opus 5.5 da30adcc2e refactor(managed-agent): fold the last CSI statement helper copies (#16503)
The CSI ready admission and application progress stores kept their own
copies of the 10 second query-timeout statement helper because #16485
was still editing the adjacent lines when #16486 introduced the shared
helper. Both now use the shared one; the timeout is unchanged.

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

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

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

Fixes #15262

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Qwen Autofix <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-11-07 08:51:30 +08:00
0a562773f7 fix(core): match one sed character for . and bracket expressions (#15701)
* fix(core): match one sed character for . and bracket expressions

The simulated sed path compiled patterns without the s and u flags, so `.`
did not match `\r` in CRLF files and astral characters were matched as two
UTF-16 code units, leaving a lone surrogate that was written as U+FFFD.

Compile with su/gsu and escape a literal ] outside bracket expressions so
the stricter u mode still accepts patterns such as [\[\]].

Closes #15675

* ci: rerun the checks on the unchanged tree

* fix(core): decline sed escapes that mean something else under u

`\p{…}`, `\P{…}` and `\u{…}` are literals to sed but Unicode property and
code-point escapes to a `u`-flagged regex, so the simulator accepted patterns
real sed rejects or leaves alone and wrote the simulated bytes to disk. Widen
the divergent-escape guard and pin the declines, plus the `u`-only rejection
class the flag introduced.

* fix(core): judge a simulated sed edit by the bytes it will write

On a CRLF file, `s/.$//` consumes the trailing carriage return, but the write
path restores CRLF, so the edit reported success while the bytes on disk were
unchanged. Compare the prepared write content against the original instead of
the parser's intermediate string.

* fix(core): judge a simulated sed edit by the content a read-back returns

The write-back prediction decoded the prepared bytes as UTF-8 and compared
them with the read content, so a file carrying a BOM or a non-UTF-8 encoding
never matched and the false `sed edit applied` this PR removes survived.
Compare content strings instead, and only predict the CRLF normalization when
the writer applies it: a client-side write stores the content verbatim, so
`s/\r$//` is a real edit there and must not be suppressed.

* fix(core): decline \0 in simulated sed patterns

`\0` is NUL to a `u`-flagged regex but a literal `0` to sed, so the simulator
found no match and answered `sed edit made no changes` instead of falling back
to the real binary.

* fix(core): predict the CRLF restoration for every delegated write

* test(core): pin the \0 decline in simulated sed patterns

* fix(core): decline mixed POSIX bracket classes in simulated sed

* feat(cli): have an Ink TUI turn await its shared tool batch (#16385)

With experimental.unifiedAgentLoop on, the stream processor hands a
shared round's tool batch to the round's submitQuery, which awaits it,
its completion step and the round that step sends, once its own stream
and activity ended. While another stream is active, which may be the
stream that sent the round (a response of provider replays alone), the
batch completes on its own as before.

- The Ink executor takes a scheduler hold: the scheduler's completion
  callback returns once the completion step stopped the turn or is
  about to send the next round, as the current path holds it, up to the
  round whose calls need the scheduler.
- A batch whose executed calls history all answered no longer sends its
  replay answers alone.

The design doc describes PR 3b-2b.

* feat(serve): add atomic user Skill activation (#16186)

* feat(serve): add atomic user Skill activation

* test(serve): cover conditional user Skill activation capability

* docs(serve): index user Skill activation endpoint

* fix(serve): preserve User Skill refresh recovery

* feat(skills): add file-backed archive installation kernel (#16421)

* feat(skills): expose native archive routes and receipts (2/4) (#16422)

* feat(skills): add file-backed archive installation kernel

* feat(skills): expose native archive routes and receipts

* chore(release): sdk-typescript v0.1.28 (#16412)

* chore(release): v0.26.8

* chore(release): sdk-typescript v0.1.28

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Shaojin Wen <[email protected]>

* fix(core): keep plain bracket classes in simulated sed

Matching `[:`/`[.`/`[=` anywhere in the pattern rejected ordinary
classes such as `[.]` -- a common literal-dot idiom that sed and the
translator both handle. Only open a POSIX construct when the opening
bracket sits inside a bracket expression, so `[^[:space:]]` and
`[[=a=]]` stay declined while `s/[.]/X/g` keeps the simulated path.

* fix(core): decline an escaped `[` followed by a POSIX-looking class

`s/\\[[:alpha:]]/X/g` is a literal `[` and an ordinary class to BSD sed, so
the simulation rewrote the file. GNU sed rejects that pattern outright as
a malformed bracket expression, so a simulated edit writes a change the
user's own sed would have refused.

The bracket scanner skipped the escaped `[` and so never saw the nested
class. Decline the POSIX-shaped lookalike outside a bracket expression and
leave a plain escaped `[`, and an escaped `[` inside a class, on the fast
path they already had.

---------

Co-authored-by: 肖洪进 <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: 2SinceJuly <[email protected]>
Co-authored-by: qqqys <[email protected]>
Co-authored-by: callmeYe <[email protected]>
Co-authored-by: Bob.qwencode <[email protected]>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-07 00:03:35 +00:00
qqqys 3f99e022b7 feat(cli): have an Ink TUI turn send the round its tool batch continues with (#16428)
With experimental.unifiedAgentLoop on, the shared tool batch's completion
step hands the next round to the turn, which takes the input queued for the
tool boundary and sends it. The batch keeps the scheduler and the TUI's
activity until then; a turn that does not wait leaves the round to the
batch. The Ink parity suite adds retry and queued-input scenarios.

Part of #15327.
2026-11-07 07:46:27 +08:00
jinyeandQwen-Coder 11c83aa21e feat(managed-agent): allow concurrent child questions (#16484)
Release local execution membership while questions wait, recover the original runtime owner, and isolate root-page action selection and drafts.

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-07 05:19:56 +08:00