1755 Commits
Author SHA1 Message Date
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
易良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 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
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
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
易良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
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
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
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
Shaojin Wen acea5b1335 docs(design): correct the daemon PR state transitions design (#16487)
Fold the unposted findings of #16246's final docs-only review, and
what the audits of those fixes turned up, into both language versions:

- v0.26.8 (2026-11-05) ships the watch:* keys of #16258, so they follow
  the same deprecation window as prWatchEvents.
- The view identity check normalises both URLs as pullRequestKey does
  (any path after the number and the port ignored) and compares host
  and number only.
- Latency is stated as nominal (one interval, or ceil(n/30) for a view
  set of n), with the cases that make it longer.
- The shared template contract moves key and template validation,
  chain resolution (event vocabulary and comment lane as parameters)
  and rendering from variable values into channel-base; %sender% is
  GitHub, %repo%/%url% keep the winner's bound URL spelling.
- Every watch-file change reads and writes within one hold of the
  per-PR lock; a daemon claims the watch only when its refresh timer
  runs and its uid owns the watch directory and files, so writes stay
  temp-and-rename; a non-owner writes nothing.
- The alias guarantees, the untrusted-primary exposure and the
  validation plan are updated to match; the Chinese twin carries the
  technical templates verbatim.
2026-11-07 02:38:45 +08:00
Shaojin Wen 301b61ddcc fix(channels): deliver plain conversation comments to PR-bound sessions (#16416)
* fix(channels): deliver plain conversation comments to PR-bound sessions

With deliverToBoundSessions on, a plain comment on a bound PR still had
to pass the repository's mention requirement before it reached the
session. On threads GitHub lists as a mention, an assignment or a review
request, the comment lane dropped it whatever the setting. On subscribed
and similar threads the bound path's preflight rejected it, and the
workaround, turning requireMention off, also made the channel answer
plain comments on unbound PRs and issues there in public. Threads the bot
account opened or commented on were not affected.

The mention requirement keeps ambient chatter out of public replies; it
is no authorization boundary. The private path now waives it through a
preflight option that admits a message whose only group rejection is the
mention requirement. The waiver covers a comment, not the PR's own
first-contact description, from a sender the repository's allowlist
names, in a repository the group gate admits, unless the sender is in
operators: a bound session usually acts on GitHub as an operator's
account, and its own comments would come back to it as turns. Sender,
repository, pairing and message-route checks still run, and repositories
that admit any sender keep the requirement.

On the mention-type threads the lane admits such a comment only when the
private path would deliver it: the routes match and a target resolves
for it as a comment event, so templates keyed by the thread's reason do
not apply to it. Its task is marked private-only: it never takes the
channel path or posts a public error comment, and it is retired when no
bound session is left. Comments only the waiver admits are dispatched
after the rest of the lane and the first-contact body, so their outcome,
a deferral or a gone session evicted from the binding map, cannot change
how those are handled.

Two failure paths that predate this change no longer post a public error
comment for a bound event when the task store fails: the gone-session
deferral under a partial scan and the retirement at the deferral cap.

* test(channels): pin the default route, audit refusal and window hold of waived comments

- Admit an unprefixed plain comment through `defaultMessageRoute`.
- Keep a waived comment's key out of the cursor when an unreadable
  publication audit refuses its dispatch, and deliver it once the audit
  reads again.
- Feed the first-contact body on the re-read when a failed waived
  delivery holds the notification window.
2026-11-07 02:34:31 +08:00
Shaojin WenandQwen-Coder dd4515ca27 feat(ci): isolate triage agent credentials and run all triage on ECS (#16468)
* feat(ci): isolate triage agent credentials and run all triage on ECS

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

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

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

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

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

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

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

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-06 18:17:45 +00:00
jinyeandClaude Opus 5.5 1ea05364a6 perf(managed-agent): take tool execution admission off tenant placement (#16478)
* docs(managed-agent): design tool execution admission without tenant placement

Admission still takes tenant placement on every tool call. It orders
admission's plain read of the harness drain fence against
requestHarnessDrain, the only fence writer that holds no row admission
takes. Lifecycle phases already hold the Session root, and the storage fence
is backstopped by migration's retirement check. The design has the drain
take the Session root after placement, and lets an ordinary session-class
admission with an existing root skip placement, taking the root as its first
statement so a REPEATABLE READ snapshot cannot predate the drain.

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

* docs(managed-agent): record the storage alias order blocker for placement-free admission

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

* perf(managed-agent): take tool execution admission off tenant placement

Every tool call still took tenant placement in admitExecution. Placement
ordered admission's plain read of the Harness drain fence against
requestHarnessDrain, the only fence writer holding no row admission takes.

requestHarnessDrain now locks the Session root after placement. An ordinary
session-class admission with an existing root, no lifecycle authority and no
storage or LOCAL storage skips placement and locks that root as its first
statement, so its REPEATABLE READ snapshot cannot predate a committed drain.
Other admissions take placement first, as before.

completeStoppedSessionRelease locked the storage alias and owner rows before
the binding, the reverse of admission and createSession. It now locks the
binding first, so a placement-free admission cannot close a cycle with it.

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

* docs(managed-agent): format the admission design with the pinned Prettier

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-06 10:10:58 -08:00
jinyeandClaude Opus 5.5 06a0cb3250 perf(managed-agent): take runtime binding control off the tenant guards (#16480)
Binding control took tenant placement and the publication guard for every
session-class binding, so each periodic lease renewal queued on both
tenant-wide rows. The publication guard only tells whether a CSI profile
original's retention root exists; any other binding is answered null or
refused whatever it says, so lockBindingControl now takes it for profile
bindings only.

Claim, renew and release change only the operation lease and move no binding
out of its state. Like dispatch, they now decide the CSI pin under the Session
root alone when it exists. compareAndSet keeps placement, which it takes next
and which must precede the root.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-06 18:04:36 +00:00
jinyeandQwen-Coder 45cde86998 feat(cli): align managed agent basic tools with daemon (#16445)
Expose glob in ordinary file sessions, durable todos in Shell sessions, and PDF page selection in Hosted read_file. Restore saved todo state on session reload while keeping exact current-schema validation and restricted resident/CSI tools.

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-07 00:13:05 +08:00
7cb46d9fed feat(hooks): add FileChanged notifications for local sessions (#13969)
* feat(hooks): add FileChanged notifications for local sessions

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

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

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

* test: stabilize hosted recovery and failed spawn fixtures

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

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

* test: align schema baseline and hook dispatch waits

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

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

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

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

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

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

* fix(ci): correct runtime broker callback indentation

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

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

* fix(ci): reduce standalone runtime fixture overhead

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

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

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

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

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

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

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

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

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

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

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

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

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

* ci: Prepare daemon Java cache before Node preflight

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

* fix(hooks): observe explicitly registered symlink targets

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

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

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

* fix(ci): bound RocketMQ release downloads

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

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

* fix(ci): provision missing browser test fonts

* fix(ci): quote the font probe environment

* fix(ci): prepare browser fonts without elevation

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

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

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

* fix: isolate reconnect hooks and cancellation test fixtures

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

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

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

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

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

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

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

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

---------

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: yungsen.hys <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-11-06 21:43:01 +08:00
1853b1505f fix(subscription): isolate activation and restore sign-in recovery (#16453)
* fix(web-shell): wait for session before checking subscription commands

* fix(cli): keep signed-out subscription startup in the composer

* fix(subscription): preserve model selection across sign-in

* fix(subscription): align sign-in with model selection scope

* fix(subscription): apply reasoning defaults across models

* fix(subscription): tag Qwen web links by client

* fix(subscription): prefix Qwen web client tags

* fix(subscription): attribute Qwen web links by paywall scene

* fix(subscription): recover historical sessions with saved API models

* fix(cli): remove the signed-out subscription startup notice

* fix(subscription): support standalone desktop activation and expose errors

* fix(subscription): isolate activation and session model selection

* fix(subscription): enforce auth policy and restore home login

* test(cli): preserve the default initializer mock on restore

---------

Co-authored-by: luoyun <[email protected]>
Co-authored-by: 易良 <[email protected]>
2026-11-06 20:11:52 +08:00
chencheng (云谦) 1b1f4455d9 ci: build the PR tree once and shorten the required path (#16450)
* ci: build the PR tree once and shorten the required path

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

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

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

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

* ci: keep the standalone regression on unit shard 1

Moving it did not shorten shard 1: the regression runs in the side lane,
in parallel with the CLI and core suites, and shard 1's time is its CLI
slice (576 s against 147 s on shard 4 for the same 181 files). That
imbalance is left to duration-based sharding.
2026-11-06 03:30:11 -08:00
jinyeandQwen-Coder e777fc7502 feat(managed-agent): enable automation by default (#16448)
* feat(managed-agent): enable automation by default

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

* style(docs): match pinned Prettier formatting

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-06 18:42:51 +08:00
ff775787f5 feat(channels): journal private bound-session delivery and scan health (#16237)
* feat(channels): journal private bound-session delivery and scan health

The event timeline could not tell the private delivery lane's story. A
prompt injected into a PR-bound session ended on the same `running` word
as an event still in flight, no task row named the session it reached,
and the three private watch retires plus the dead-letter left only stderr
behind — so a silent consumption looked like a quiet repository. A bound
event rejected by the channel's own gates was worse than invisible: its
skip row was followed by running rows, reading as a delivery.

The private delivery method returned a boolean meaning "settled", which
conflated an injected prompt, a private retire and the public fallback.
It now returns a verdict, and the task runner derives the journal's
terminal word from it, so an event that was never delivered can no longer
be recorded as delivered. Routing, fallback, retry-budget and cursor
decisions are unchanged.

The binding scan is a new hard dependency: while it has never succeeded
the cold-start gate stalls every notification, yet the poll reports ok.
Degraded rows now surface that beside the stderr lines the adapter
already throttles, and on the edge of the existing watch-reconcile flag,
so a permanent outage cannot flood the journal. Attaching this to the
poll detail was rejected: both cold-start gates return before the detail
is set, which is exactly when it would matter.

No new event type and no protocol change — an outcome is already an open
string. The dead-letter's message body and its failure-path payload dump
stay out of the journal. Also labels `failed_ambiguous`, which was
already written but rendered as a raw token.

* fix(channels): make the bound-delivery journal claims true

Review round 1 found sixteen places where the journal, its labels or its
documentation promised something the code did not do. All sixteen are
addressed here; none is deferred.

Three were ordering or attribution defects in the new rows. A watch retire
was journaled before the removal it describes, so a throwing removal left a
terminal claim for an event the next poll re-runs and retires again under a
different reason. Rows written while a delivery is cleared lost the session
they were about, which is the one fact the private lane's attribution adds.
And the pre-claim watch retire sliced the loaded state before saving with no
rollback, so a failed save lost the event with no trace at all — the comment
claimed the opposite.

Two were counts that measured something other than their label: the partial
scan published the routing map's size, which counts bound PRs, as a session
count; and the streak advances per binding scan, not per poll, because a
gone-session retry refreshes again inside the same poll. The session count now
comes from the scan's own rows, and the streak is labelled as scans in both
languages and in the stderr lines that share the counter. Re-cadencing the
counter itself would change the throttle timing the previous PR shipped, so
the label moved instead.

Two were missing surfaces: the pending-queue-overflow retire wrote only
stderr and the dead-letter file, and an unreadable watch state was reported
as a pending backlog whose stated remedy cannot clear it. The overflow now
journals a body-free skip, and the watch guard classifies a read failure
separately while its gate stays fail-closed.

The recovery signal the prose promised did not exist: a poll/ok row is
written after every poll that does not throw, including all of an outage, so
it cannot mark the end of one. The watch guard now journals its clearing
edge, and the prose says why ok rows cannot serve.

The rest were documentation and test-coverage corrections: the protocol
reference promised detail.streak on every degraded row when only the
binding-scan rows carry it; the design file map certified the protocol
reference as unchanged while this branch edits it; the override's stated
consequence was backwards; and five guards had no witness or only half of
one. Each now has a test, and the suite grew from 438 to 444 in the GitHub
package.

Verified by 37 mutations, all killed. Two initially survived and were
investigated rather than accepted: a loose sessionId count that three
unattributed-row-free rows still satisfied, and a filter that matched no test
after a rename. Both were tightened until the mutation died.

* docs(channels): align the journal ledger with the round-2 verification

The validation section still cited the round-1 mutation count after the
review fixes added fifteen more, and the Chinese half had lost the count
altogether during the rewrite. Both now state 37 applied and 37 killed.

Also records why two mutations initially survived and what was done about
them, since a sweep that cannot distinguish "no test ran" from "the guard
held" is not evidence: a loose sessionId count that three correctly
attributed rows still satisfied, and a filter left matching nothing by a
test rename. The Web Shell line now mentions the absence coverage too.

* fix(channels): keep the lost bound session named on every journal row

The second review round of the bound-session journal found fifteen gaps.
Three were consequential, and two of those were introduced by this PR's own
previous round:

The gone-session retry never updated the local delivery, so with two
followers both gone the terminal row named the first one — a session evicted
a step earlier — while the row above it named the last. An operator reading
"which session lost this event" would open the wrong workspace.

Remembering the target only for the transition that clears the delivery is
not enough: once the clear is persisted, every later poll re-enters the
absence gate with no delivery to read, and the dead-letter at the deferral
horizon reads a record that has none either. A driven run of that sequence
left 239 of 242 rows unattributed, with the terminal row naming nobody — the
one fact this lane's attribution exists to supply. The lost target is now
persisted as attribution only; the delivery is deliberately not restored,
since that would re-enter bound delivery and change routing.

The overflow retire was journaled before its queue shift was durable, with
no rollback, so a failed save left a skip row for an event a restart would
later deliver. It now saves before it journals and restores the shift when
the save throws, matching the sibling pre-claim path.

Also: the partial-scan session count now includes the sessions retention
keeps routing (a draining workspace lists none while still delivering to
three), and both pre-claim retires carry the frozen target so the timeline
and the dead-letter agree about the same event.

Four coverage gaps whose mutants left the whole suite green are closed: the
guard keeping the partial-scan row off the cold-start path, the session on
the no-candidate retire, a second healthy poll so the recovery row's edge
trigger is distinguishable from an unconditional write, and both a shape
test and a Chinese render test for the interpolating labels.

The docs said more or less than the code: detail.sessions is carried by one
row, not by every partial scan; the normative sessionId definition read a
retire chip as a delivery; the emit-point inventory missed the overflow
site; and bindings_partial was described as recovering implicitly when a
successful multi-page scan is its producer. Auditing that last correction
found the stderr census was still incomplete — the lane has five recurring
health and capacity sites, not three — so it now names all five, ranks them
by consequence and states its own scope.

* docs(channels): define what the partial-scan session count actually counts

The count became listed-plus-retained when retention was included, but three
surfaces still described the old value. The protocol reference called it "the
sessions the map still routes to", which is wider than the code: the map also
skips channel-owned sessions and unparseable binding URLs. Both design halves
still said it was "the scan's own row count", which is narrower than the code
and was the exact wording the review asked to correct.

Define it as a session-row count — the rows this scan listed plus the sessions
retention keeps routing, bounded by the retention cap — and say explicitly that
it is not the number of entries surviving the map's own filters. Also record
that "listed" in the row label and in the stderr line is shorthand for
listed-or-retained and that both print the same number, so the wording cannot
quietly drift apart from the value again.

Documentation only: the adapter and its tests are byte-identical to the previous
commit, so every runtime result reported for this branch still applies.

* fix(channels): close the review gaps in the private-lane journal

Address the eight open suggestions and the five deferred findings from
the round-3 review, each with a mutation-verified guard (10 new mutations,
10 killed; one cap-count fixture initially survived because the first
empty partial already capped lastBindings, and was rebuilt so retention
is above the cap exactly at the emitting scan).

- R1-15: the overflow dead-letter append and the sidecar save now fail
  independently. A failed save rolls the shift back, names the watch-state
  path instead of blaming the dead-letter file, drops the payload dump
  (the body is already durable), and remembers the head so the next
  detection skips the re-append. The save-throws test now drives a second
  detection and asserts exactly one dead-letter line.
- R2-6: the pre-claim skip comment and both design languages now say only
  the overflow retire writes a dead-letter record; watch_no_candidate and
  no_role_matched leave the skip row as the event's only trace.
- R2-3: bindings_cold_empty no longer claims implicit recovery — an empty
  partial is a successful scan, the streak resets only on a complete or
  non-empty snapshot, and past the escape the silence is the throttle
  maxing out, not recovery.
- R3-1: a 1100-session fixture pins detail.sessions to the 1024 retention
  cap at the emitting scan; swapping retainedCapped.length for
  retained.length reds it while the two sibling count tests stay green.
- R3-5/R3-6/R3-15: the design's outcome cell lists failed_ambiguous and
  degraded, the watch-guard bullet admits the read-failure error field,
  and the sessionId carve-out covers every row written on a clear plus the
  pre-claim skip; the chip sentence says reached-or-lost. Both languages.
- R3-7: the three private-retire watch tests assert the public path stayed
  empty; leaking the envelope through handleInbound reds them.
- Deferred: the gone-horizon attributes the in-memory task before retiring
  and the dead-letter record names the lost session; the watch-guard row
  re-journals when the blocking cause changes; the subscriptions-off
  recovery edge has a test; the recovery row renders Recovered/已恢复
  instead of Degraded (wire outcome unchanged); a gone-retry fixture pins
  the scan-scoped streak at 10 inside poll 4.

Verified: channels/github 595 passed, web-shell channels 343 passed,
eslint and prettier clean; design mutation ledger updated to 47/47.

* fix(channels): rework the overflow retire ordering and close round-4 review

Critical R4-1: the overflow branch saved the sidecar mid-pass, committing the
advanced baseline before the pass's later events were queued; a crash then
never re-derived them. Remove the mid-pass save entirely — the head is sliced
in memory only, and the baseline and full queue commit together in the single
end-of-pass save, with the skip rows journalled after it.

The dead-letter record is now the retire's commit point. A lazily loaded,
file-backed key set (restart-durable, replacing the process-local map) skips
the re-append (R4-16), and both drains recognize a dead-lettered head and
complete its retire instead of delivering it, so no event is both delivered
and filed as retired undelivered (R1-15 follow-up).

Also addressed:
- R4-12/13/14: terminal, transition lost-session and pre-claim rows carry
  sessionWorkspaceCwd so Open-session reaches a session in another workspace.
- R4-11: the no_role_matched comment states the real ordering — the dedupe
  cursor is in memory until the base saves it, so the skip row is at-least-once.
- R4-15: already_delivered_other_role is documented as a defensive,
  unreachable guard and dropped from the published vocabulary.
- R4-2/3, R3-5: §1 outcome cell adds suppressed/posting, reason covers task
  and poll rows, sessionId carve-out adds cancelled and distinguishes live
  running rows. Both languages.
- R4-4/5/6: the pre-claim census acknowledges the role-aware drain's
  pre-existing fourth retire; the retire-carries-session rationale notes the
  preflight exception; the dead-letter write-failure note names the
  non-recoverable public-suppression exception. Both languages.
- R2-3/R4-7: cold-empty reset is keyed on the routing map and trust flag, and
  the escape's lane-open claim is qualified for the refresh-failure producer.
- R4-8/9: the recurring-site census adds the credential/rate-limit pass abort
  and corrects the no-existing-flag claim for the stale-map refresh failure.
- R4-10: watch-guard error wording covers reconciliation failure too.
- R4-17/18/19: openTarget joins the interpolation guard with a ZH render
  assertion, the EN streak label says consecutive, and ZH degraded/retired/
  recovered labels get render assertions.

Verified: channels/github 601 passed, web-shell channels 331 passed, eslint
and prettier clean; 4 new mutations killed (mid-pass save, file-dedupe, drain
guard, workspace lookup); design ledger updated to 51/51.

---------

Co-authored-by: verify-round <verify-round@local>
Co-authored-by: verify-pr <verify@local>
2026-11-06 18:21:47 +08:00
chencheng (云谦) a1d713e747 feat(cli): report interactive program status with OSC 7501 (#16407)
* feat(cli): report interactive program status with OSC 7501

Report a single root OSC 7501 record from the interactive UI so terminals
and agent inboxes can show whether Qwen Code is working, waiting on the
user, or finished, without parsing the screen.

- idle / working / blocked (permission, question, auth) / done / error,
  with app=qwen-code on every report
- done and error latch until the next turn; a cancelled turn returns to
  idle; done and error are kept on exit, everything else is cleared
- msg names the tool at most, never its arguments or model text
- TTY only, deduplicated, retried after a failed write; never wrapped for
  tmux passthrough
- cleared while an external editor owns the terminal, rewritten on return
  and on SIGCONT

Closes #13870

* fix(cli): latch the OSC 7501 turn outcome when the turn ends

The outcome was recomputed from a history index on every history change,
so an error added while idle (a failed slash command) flipped `done` to
`error`, and memory compaction reset it to `idle` or hid an error from
the turn. Decide it once when streaming state returns to Idle, bound the
turn by item id, and reset only when history is cleared.

Also count a blocked prompt and a stop-hook loop as failed turns, and pin
the msg-only report, release/restore and editor call sites with tests.

* fix(cli): reset the OSC 7501 outcome on /clear and cap msg by decoded size

/clear only masked the latched outcome: the tracker kept `done` or
`error`, so the next item added while idle brought it back, and exit
could keep a stale badge. Reset the tracker when history empties.

The msg limit checked only the encoded length, which lets a 2049-byte
message through as 2732 base64 characters. Cap the decoded text at 2048
bytes, which also bounds the encoded form.

Correct the design doc: a slash command runs as a turn and decides its
own outcome; it is not an idle history change.

* test(cli): assert the decoded msg is present before measuring it

decodeMsg returns string | null, which tsc --build rejects as a
Buffer.byteLength argument.

* fix(cli): ignore errors already pending when an OSC 7501 turn starts

A terminal API error stays in the pending area until the next submit
commits it, and a Goal submission skips that commit. The next Goal turn
then ended with the old error still pending and latched `error`, which
exit kept. Record the failure items pending at turn start and count only
new ones.

Also restore the program status before the raw-mode restore in
useLaunchEditor, which can throw; say control characters become spaces
in the msg encoder; and note in the design that the OpenTUI renderer
does not report status yet.
2026-11-06 17:34:40 +08:00
jinyeandQwen-Coder 32d22f950c feat(managed-agent): support durable structured user questions (#16433)
Connect Hosted question actions, structured answers, durable continuation, and the WebShell question form. Cover canonical response replay and pending-question recovery across Harness and Java context restarts.

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-06 09:23:44 +00:00
易良andQwen-Coder 1a20792cfa ci: route the no-AK integration gate to ECS for every pull request (#16132)
* ci: route the no-AK integration gate to ECS for every pull request

Same trust class as lint_and_static (2026-11-02 extension of the
2026-10-31 decision): the integration step executes PR code, but the
lane carries zero secrets by construction — every model key is emptied
at the step and the job's only token is the read-only checkout grant.
The gate's step-level runner.temp HOME becomes run-unique and is
cleared before use (the R2-1 lesson: RUNNER_TEMP survives across jobs
on this pool). test and integration_cli keep the association-gated
ubuntu_runner. Mutation-verified: runs-on revert, fixed HOME, and a
real key in the PR-code step each red the routing battery.

* fix(ci): isolate no-AK HOME from the first step; value-pinned zero-secrets

Round-2 review findings: (1) Critical — the gate-scoped HOME missed the
lane's actual fork-code start: checkout's local action and the install
(lifecycle scripts plus scripts/prepare.js, un-skipped) ran under the
runner's real shared home with a shared COREPACK_HOME — the exact
cross-job poisoning channel the lint acceptance closed. Mirror lint:
first-step GITHUB_ENV run-unique HOME + COREPACK_HOME inside it +
shared verified XDG cache + always() reclamation. (2) Critical — the
zero-secrets guard was name-based and missed odd-named secrets; enforce
by VALUE (every non-path env key empty, no secrets.* anywhere in the
gate). (3) runs-on literal pin tightened (timeout line also mentions
lint_runner). Docs: EN single-lane phrasing, stale merge-queue claim,
2026-11-02 paragraph rewritten for the first-step shape. Six mutations
red (first-step removal, shared corepack, odd-name secret, no cleanup,
runs-on revert, fixed HOME).

* docs(ci): correct the enforcement-order claim and stale trust-basis note

Round-2 review: the doc claimed the routing battery runs before
dependency install — false (HELPER_TESTS imports yaml, runs after
install, and not at all on github_ci_only PRs' own runs); and the
ci.yml trust-basis comment still described the round-1 gate-scoped
HOME shape. State the actual timing and detection latency, and the
shipped first-step shape.

* fix(ci): scrub ANTHROPIC_AUTH_TOKEN at the no-AK gate; honest cache naming

Round-3 review: the Anthropic SDK's auth resolver back-fills
ANTHROPIC_AUTH_TOKEN from env, so a runner-account residue would survive
into fork PR code on the pool — add it to the gate's scrub list (pinned
by both suites). The 'Configure persistent npm cache' step name promised
persistence the per-run HOME removes; rename it and record the accepted
cold-install cost (shared pnpm store stays a follow-up).

* fix(ci): scrub ANTHROPIC_AUTH_TOKEN at the no-AK gate; honest cache naming

Round-3 review: the Anthropic SDK's auth resolver back-fills
ANTHROPIC_AUTH_TOKEN from env, so a runner-account residue would survive
into fork PR code on the pool — add it to the gate's scrub list. The
value whitelist only iterates present keys, so the deletion mutation
escaped it; pin the key's presence-and-empty in the battery and the
28-key literal list. The 'Configure persistent npm cache' step name
promised persistence the per-run HOME removes; rename and record the
accepted cold-install cost (shared pnpm store stays a follow-up).

* test(ci): pin the no-AK grant, HOME template and reclamation path

Three assertions in the no-AK routing battery proved less than they read:

- the job's `permissions` was never inspected, so the read-only-checkout
  grant that justifies routing untrusted fork code onto the pool could be
  widened without a red test;
- `create.includes('HOME=')` is satisfied by `COREPACK_HOME=` and
  `XDG_CACHE_HOME=`, so deleting the real HOME/USERPROFILE publication kept
  the test green;
- the cleanup only had to contain the `qwen-no-ak-home-` prefix, so a path
  that dropped `-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}` would still pass
  while removing a directory nothing used.

Pin the grant by value, the publication by its printf template, and the
reclamation by the exact run-unique path the create step builds.

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

* test(ci): pin the realized HOME values in the shared ungated lane loop

R2-1: assert the create `home=` line, the gate QWEN_HOME and the cleanup
command by value, and require the exact set of `$GITHUB_ENV` writers. An
`echo` stand-in for the cleanup, a `-v2` create path and an appended second
HOME writer all kept the battery green under containment checks.

R1-2 / R1-1: hoist those pins into the shared `ungated` loop so
`lint_and_static` gets them too. Its create step was pinned by
`includes('HOME=')` (satisfied by `COREPACK_HOME=`) and its cleanup by
`includes('qwen-')`, so a dropped `HOME=%s` or a retargeted `rm -rf` survived.

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

* test(ci): red on later republishes of the isolated HOME keys

R3-1: the GITHUB_ENV equality guard read step 0 only, so a later step that
republished HOME reverted the run-unique home by last-write-wins while every
assertion stayed green — the isolation would silently stop applying to the
steps that run PR code. Scan the remaining steps for isolated-key
$GITHUB_ENV writers and for step-level HOME/USERPROFILE/COREPACK_HOME/
XDG_CACHE_HOME overrides, leaving the npm cache step's own key a legitimate
later write.

Mutation-verified on this head: appending printf 'HOME=%s\n' ... to
integration_no_ak's 'Configure per-run npm cache (self-hosted)' step and
adding a step-level `env: HOME` each turn the suite red (103 pass -> 102
pass / 1 fail); reverting restores 103/103.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-06 16:41:48 +08:00
jinyeandQwen-Coder 738c4bbf7e fix(serve): bound command snapshot replay memory (#16413)
* fix(serve): bound command snapshot replay memory

Suppress unchanged command snapshots and retain only the latest full skill details so periodic refreshes cannot grow daemon heap indefinitely. Bound replay rings by serialized bytes while preserving ACP live delivery and reconnect IDs, and expose retention diagnostics.

* fix(serve): avoid redundant replay seed serialization

Size only retained complete command snapshots when seeding replay history, and forward the replay byte limit through the development daemon launcher. Pin snapshot accounting and lifetime eviction counters with independent regression assertions, and document the count/byte retention bounds and conditional command notifications.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-06 16:13:17 +08:00
ytahdnand钉萁 255d5cd2ae feat(web-shell): polish composer action menu presentation (#16181)
* feat(web-shell): polish composer action menu presentation

* style(web-shell): use regular composer menu labels

* fix(web-shell): address composer menu review feedback

---------

Co-authored-by: 钉萁 <[email protected]>
2026-11-06 06:47:28 +00:00
Shaojin WenandQwen-Coder 3663209c3d chore(ci): shorten stale PR window to 30 + 14 days (#16435)
* chore(ci): shorten stale PR window to 30 + 14 days

The open PR backlog keeps growing, and the 60 + 30 day window only reaches PRs inactive for two months, leaving the 30-60 day band untouched. Lower the stale threshold to 30 days and the close window to 14 days, and raise operations-per-run to 300 so the first sweep can label the backlog in one or two runs. The stale/close messages and the fleet-shepherd design docs are updated to match.

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

* docs: keep the stale-window reference in the design current

Avoid pinning the note to a merge-date guess: name the policy change instead, and reflow the paragraph to the file width.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-06 14:43:07 +08:00
jinye 6fb741642f perf(managed-agent): take tool execution control off the tenant guards (#16415)
Every tool call admits an execution and authorizes its dispatch. Both went
through lockBindingControl, which for a session-class binding (the default
isolation class) took tenant placement, the publication guard, every slot and
binding row of the Session and then its root, only to prove the Session is
not CSI-pinned.

lockExecutionControl now decides that under the Session root alone: the pin
lives on the root, and profile slots and bindings require a committed pin.
lockManagedSession and requireProvisionPin lock the root before slot and
binding rows, so every placement owner shares the root-then-binding order
that guard-free dispatch uses. Without an existing root, a locking read takes
only a gap lock and orders nothing; dispatch then keeps the guarded path (a
MySQL stress test reproduced the deadlock between the binding's unique-index
and primary-key lookups otherwise). Admission still takes placement for the
harness drain fence.
2026-11-06 06:25:42 +00:00
callmeYe 17ad9cdb04 feat(skills): add file-backed archive installation kernel (#16421) 2026-11-06 11:30:04 +08:00
callmeYe 9b1ce46d77 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
2026-11-06 11:18:44 +08:00
qqqys 4cea3a792f 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.
2026-11-06 10:31:47 +08:00
曹潇缤andShaojin Wen ecc64a735a feat(qqbot): deliver proactive loop messages as active group messages (#15635)
* feat(qqbot): deliver proactive loop messages to the group target

* fix(qqbot): fail blocked proactive deliveries and post loop output once

- pushProactive now raises ChannelProactiveDeliveryError when
  sendMessageWithReplyContext reports a blocked outgoing route: the
  blocked send resolves instead of throwing, so a loop delivery that
  never reached the wire was being recorded as delivered. The block's
  disposition is kept so the scheduler can distinguish a permanent
  route failure from a retryable one; ACTIVE_MSG_DISABLED still throws
  unwrapped from the send path.
- QQ's streaming chunk sink is skipped for unattended turns (loop and
  webhook task, identified by the new protected isProactivePromptTurn
  predicate), so the full answer is posted once through pushProactive
  instead of being streamed to the group first and posted again. Base
  chunk bookkeeping, lifecycle events and attended streaming are
  unchanged.
- Tests: permanent and transient route blocks reject the loop and never
  send; streamed chunks of an unattended turn produce exactly one send
  carrying the full response.

* fix(qqbot): keep permanent delivery errors permanent and document proactive scope

- pushProactive rethrows QQ's permanent DeliveryError codes
  (RETRY_EXHAUSTED, ACTIVE_MSG_DISABLED, FALLBACK_FAILED) as
  ChannelProactiveDeliveryError('permanent', ...) so the daemon boundary
  classifies a group that cannot receive active messages as rejected
  instead of retryable. RATE_LIMITED stays transient.
- Drop the threaded-target guard copied verbatim from the base override;
  every entry point already gates on supportsProactiveTarget and QQ never
  produces a thread id, so the copy was unreachable drift.
- Name all four callers in the pushProactive doc comment (loop, webhook
  task, background/named-task response delivery, daemon deliverProactive)
  and state that background responses intentionally go out unanchored now.
- Document QQ in the proactive-delivery capability matrix and in the QQ
  channel guide: active messages, group quota, ACTIVE_MSG_DISABLED and
  the 5-failure auto-disable, no threaded targets, learned chat type
  required.

* feat(qqbot): retire loops for chats that can no longer receive proactive messages

A loop created before a group disabled active messages, or before the bot
was removed from the group, kept firing: each run rebuilt a session, ran
a full unattended agent turn, and could never deliver. Both chat-wide
state changes now retire the chat's loops.

- base: add an optional chat-scoped controller lookup (listForChat) and
  protected disableLoopJobsForChat(chatId), which disables every enabled
  loop in a chat regardless of the creator's exact target; the CLI
  controller implements the lookup over the loop store.
- qqbot: handleGroupDelRobot folds the disabled count into its existing
  cleanup log line; handleGroupMsgToggle disables the group's loops when
  active messages are turned off (both handlers become async and are
  dispatched with an explicit rejection log).
- qqbot: refuse proactive targets for a group whose active-message
  toggle is known to be off (supportsProactiveTarget), so loop creation
  is rejected up front and a firing refuses before burning an agent
  turn; direct messages never consult the group toggle.
- tests: creation refusal via the loop tool, run refusal with the prompt
  never called, removal and toggle-off teardown against the real
  controller, DM exemption, mid-flight toggle classification, and the
  base helper's contract.

* test(qqbot): keep the loop-store integration test honest

- Pin a rendered next-fire ISO timestamp instead of base's 'n/a'
  fallback: the copied controller now delegates nextFireTime the same way
  the production controller does, and the store clock is fixed so the
  value is deterministic.
- Redirect QWEN_HOME at the test's tmpdir before the channel constructor
  runs and assert the channels directory lands inside it, so the test no
  longer creates ~/.qwen/channels on the machine running it.
- Resolve the session through the locally constructed SessionRouter
  instead of reaching the protected ch.router, which was a TS2445 that
  no gate in the repo catches (test files are excluded from tsc).

* fix(qqbot): close the round-2 proactive delivery findings

- Resolve the target chat type through chatTypeFor() instead of
  chatTypeMap, and refuse an explicit group target whose type is unknown,
  so config-declared groups and never-learned groups are gated on the
  same type resolveRoute routes on instead of burning an agent turn that
  cannot deliver.
- Route background/named-task responses through pushProactive
  unconditionally: a toggle-off group must not silently receive them
  through the anchored passive reply path.
- Extract one PERMANENT_DELIVERY_CODES/isPermanentDeliveryError predicate
  and use it at the proactive seam and the five existing classification
  sites, preserving each site's behaviour.
- Fix the two comments that named a scheduler disposition check that does
  not exist (the daemon classifyChannelDeliveryError is the only reader)
  and word the blocked-route error as 'outgoing route unresolved
  (<disposition>)'.
- Include the group openid in group-event failure logs and pin the
  websocket dispatch rejection guard with a corrupt-store test.
- Stop retiring a group's loops when its active-message toggle turns
  off: the toggle is reversible, the pre-flight gate already prevents the
  turn burn, and the scheduler's 5-failure auto-disable retires the job.
  Retirement stays on bot removal, where the gate cannot see the change.
- Pin the permanent/transient arms with FAILED-fallback and RATE_LIMITED
  tests.

* test(cli): pin the chat-scoped loop lookup

listForChat decides which loops chat-wide teardown retires, and the QQ
suites substitute hand-rolled controllers whose filter mirrors it. Test
the real controller over a real ChannelLoopStore seeded with two chats
and two channel names, so dropping the filter (or only its channelName
clause) reds instead of silently retiring unrelated users' loops.

* docs(qqbot): record the proactive delivery contract

- Add the design record in English and Chinese covering the capability
  flip and the four base call sites it activates, the single-delivery
  rule for unattended turns, permanent/transient classification, chat
  retirement on bot removal, and the group active-message toggle
  contract.
- Complete the opt-in adapter list (DingTalk Workspace, Email with its
  proactiveRecipients qualifier) and state QQ's target restrictions and
  the unanchored background-response behaviour.
- Document isProactivePromptTurn in the base hook table and the
  adapter-author proactive delivery section.

---------

Co-authored-by: Shaojin Wen <[email protected]>
2026-11-05 18:28:31 +00:00
Shaojin Wenandverify-round e9a20d6ed7 fix(core): keep session list titles when a transcript is rebuilt (#16299)
* fix(core): keep session list titles when a transcript is rebuilt

The sidebar label came only from the transcript: a custom_title record or
the first user prompt. A transcript recreated without its head (missing
file recreated on the next append, crash truncation, external cleanup) lost
the prompt, so displayName became empty and the Web Shell rendered the
session id prefix.

Persist the list label to <sessionId>.title.json next to the transcript:

- the first user prompt (source "prompt");
- custom titles from ChatRecordingService and SessionService.renameSession
  (source "manual"/"auto").

Reads keep the transcript authoritative and fall back to the sidecar.
Archive, unarchive, delete, and storage relocation move or remove the
sidecar with the session. Web Shell rows now render "Untitled session" /
"未命名会话" instead of an id prefix.

Design: docs/design/session-title-sidecar.md

* fix(core): keep the list-label sidecar best-effort

Write it after the transcript append and swallow its errors, so a partial
Config or an unwritable home can never drop a user message. Update the
deletion-cleanup ownership assertion for the extra title-sidecar check.

* fix(core): align the title sidecar with the list reader

- Seed the sidecar when a transcript is resumed (custom title first, then the
  recorded first-prompt display text) so sessions created before the sidecar
  existed gain one without another prompt.
- Write the sidecar when forkSession commits a titled fork.
- Store the prompt's displayText when present and share the reader's
  200-code-point-plus-ellipsis truncation instead of a UTF-16 slice.
- Pin the intended behaviour with tests for a text-less first prompt, resume
  seeding, and a titled fork; update both design documents.

* fix(core): close the review findings on the title sidecar

- Seed a resumed session with its actual first prompt: track that text outside
  the 20-entry title window and fall back to the record's text part when no
  display projection exists.
- Never replace a persisted label from a later prompt. This also lets an
  image-only first prompt leave the slot open for the next text prompt.
- Truncate and validate the sidecar name by code point so an astral prompt
  cannot store a lone surrogate or a discarded ellipsis.
- Reconcile the title sidecar in both archive/unarchive conflict-resolution
  branches, where the hand-rolled sidecar set previously left it orphaned.
- Update the workflow-mode e2e locator for the untitled row label, and cover the
  primary and fanout rows, archive moves, and storage relocation.
- Correct both design documents: the list reads the sidecar when either the
  title or the prompt is missing, and a wholesale chats-directory clear also
  removes the sidecar.

* fix(core): keep sidecar-backed session names consistent

Review round follow-ups for the session title sidecar:

- wrap the archive/unarchive conflict reconcile so a sidecar IO error no
  longer reports an already-completed transcript move as failed;
- also fall back to a manual/auto sidecar in findSessionsByTitle, the
  picker prefix scan, and the unarchive collision guard, so a name the
  sidebar shows can be found with --resume and cannot collide unnoticed;
- converge the live write and the resume seed on one label policy that
  mirrors the list reader (explicitly empty projection, Mod prompts);
- stop a projector-restored session from being labelled with its first
  post-resume prompt, which is not the session's first prompt;
- copy the source sidecar into a fork made without an explicit title;
- return no name instead of another project's prompt text when the
  transcript is refused for project membership;
- keep the id in the fan-out tooltip and the read-only row's accessible
  name, where it is not the visible text.

Tests cover each fix, including the archived half of the lifecycle and a
writer/reader pair on one filesystem; design docs updated in both
languages.

---------

Co-authored-by: verify-round <verify-round@local>
2026-11-06 02:24:26 +08:00
jinyeandClaude Opus 5.5 c4f9899eea perf(managed-agent): take native journal snapshots off the tenant guards (#16410)
lockSnapshotWriter took tenant placement and the publication guard on each of
its three calls per snapshot, so every snapshot of a tenant queued behind
binding transitions, provisioning, retention, maintenance, collectors and
writer acquisition. It now uses the guard-free publication writer.

The build transaction cannot lock the Session root first, because its
nested journal reads take read leases on that root and head. When it reaches
the root it holds only the snapshot's own new rows. Any transaction that
could hold the root and wait for those rows already closes a cycle with it
today through the guards, so dropping them adds no cycle. Root and head
order the writer checks, and the read lease keeps the copied resources. A
pinned CSI Session still takes both guards behind its root.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-05 18:09:10 +00:00
Shaojin Wen a32646e640 docs(design): propose PR state transitions from the daemon PR refresh (#16246)
* docs(design): propose PR state transitions from the daemon PR refresh

The daemon's PR refresh already sweeps every bound PR with the host's
gh credential; the GitHub channel polls the same PRs again to detect CI
failures, merge conflicts, head changes and merged/closed transitions
for bound sessions, with its own credential, schedule, CI definition
and state machinery, and only where such a channel is configured.

Propose observing head, CI and mergeability in the refresh, keeping the
snapshot and pending transitions in the PR sidecar, delivering each
transition into the bound session through the existing prompt admission
and resume paths, and removing the channel watch in a second PR.

* docs(design): keep per-head CI history and daemon-wide recipients for PR state events

Two inconsistencies with the channel watch the proposal replaces:

- A single ciFiredSha lost earlier heads, so a force-push back to a head
  whose CI failure had already fired would fire it again. The sidecar
  now keeps a bounded ciFired ring (16 heads per PR), the daemon's
  equivalent of the channel's durable fired ring, and the acceptance
  criteria pin the revisit case.
- Recipient selection was per workspace, so two registered workspaces
  for one repository would each deliver to their own bound session. The
  sweep that detects a transition now selects one recipient across all
  trusted workspaces, dispatches through that session's runtime, and
  writes the snapshot and fired record to every sidecar bound to the
  PR so sibling sweeps observe no transition.

* docs(design): give prWatchEvents a deprecation window now that v0.26.7 ships it

The proposal justified rejecting prWatchEvents at connect() with "no
tagged release contains the channel watch". That premise is stale:
#16117 merged on 2026-11-03 and v0.26.7 (2026-11-04) ships and
release-notes it. PR B now keeps accepting the field for one minor
release (one warning naming sessionPrWatch, no watch pass), and the
following minor release rejects it; acceptance criterion 3 and the files
table follow. The refresh-interval note no longer claims a one-minute
floor: sub-minute values fall back to the default and 0 disables the
sweep.

* docs(design): reconcile sibling PR snapshots before detection

Propagating a snapshot to every sidecar bound to a PR is a sequence of
separate file writes, so a crash between the recipient's write and the
sibling writes left a sibling one observation behind; its next sweep
re-detected the same transition under a fresh key and the recipient was
admitted twice. Detection now takes the newest sibling snapshot (by a
new observedAt field) as its baseline, so a lagging sibling catches up
silently and the original pending key is the only one for the episode.
The durability section lists this fourth crash window, and the
validation plan and acceptance criterion 5 pin it.

* docs(design): version slim-listing state observations for baseline recovery

The newest-sibling baseline only worked for observations made by
gh pr view: merged and closed states come from the slim listing and
left observedAt unchanged, so after a crash between the recipient's
write and the sibling writes the two sidecars carried the same version
and recovery depended on enumeration order. Every observation that
changes the watch block now stamps observedAt, the block carries the
state seen at that version, and the baseline is the whole block of one
observation. The validation plan covers the terminal-state crash case.

* docs(design): PR-scoped pending, provenance-bound admission and sweep budgets

Folds in the findings of the docs-only review on 8032f568bb:

- pending is PR-scoped: written to every active sidecar bound to the PR
  and drained once per key to a recipient resolved at delivery time, so a
  recipient archived or outranked between detection and drain loses
  nothing; a lagging sibling adopts the newer block with its pending
  entries.
- the daemon-origin classification is a trusted in-process parameter of
  the admission call, never a string read from the request body; the
  marker name only enters the strip lists.
- the GitHub view loop reports its own rotation offset, runs under the
  Aone aggregate view budget, isolates per-PR failures, dedupes views
  across workspaces by freshness, and does nothing when the setting is
  unset; the watch block gets its own mutation helper because
  updateSessionPrStates returns early without a state/issues change.
- over-length keys hash like the channel's; a partial scan skips
  detection; siblings are re-resolved for trust and generation before a
  write; the A→B window skips repositories owned by a configured channel
  watch; wakeDormant: false keeps entries until the next live turn; the
  API budget is stated per workspace; the "identical rules" claim carries
  the CI-predicate difference; validation and acceptance cover each.

* docs(design): never re-target an admitted PR event key

With the recipient reselected on every drain, a key admitted to one
session could be admitted again to another when a replica clear failed
and the most recently active binder changed before the retry: the
prompt route's admission ring is keyed by session and key, so it cannot
suppress that. The drain now records admitted keys per PR for the
daemon's lifetime and only retries the cleanup for them; lazy recipient
selection applies to keys not yet admitted. Validation and acceptance
criterion 2 cover the partial-clear/recipient-change case.

* docs(design): one daemon-owned watch file per PR, switch-off rollout, channel prompt hygiene

Fold in the docs-only review of d93c3e70d8, whose findings were only in
the workflow log (1 Critical, 8 Suggestions), and the audit rounds run
on the revision.

- The event prompt applies the channel's bound-prompt hygiene:
  sanitizePromptText on every GitHub-derived line and the 6,000-code-point
  cap with a visible marker; no display text is set.
- PR state lives once per PR in a daemon-owned watch file instead of a
  replica in every binder's sidecar, so no copy can lag, be adopted or
  re-detect; the sidecars are unchanged. One daemon per global qwen dir
  owns the directory through an owner record with the channel service
  pidfile's semantics; non-ENOENT read errors fail closed for the tick.
- PR A ships the machinery behind an internal switch that stays off; PR B
  turns it on and removes the adapter watch in the same change, so no
  build runs both pollers. prWatchEvents and watch:* actions are read as
  aliases for one minor release.
- Views go by URL from a daemon-private directory, only to GitHub hosts,
  with the workspace's environment; closed PRs stay in the rotation where
  the listing does not observe them; a renamed repository is accepted.
- The rotation wraps as the Aone one does; one tick-level view deadline;
  restores run off the tick; the drain runs under the per-PR lock with
  in-flight marks; admission counts as delivery.
- Role-aware actions (#16258) are carried over; the restore-failure
  split matches the channel's Gone set; partial scans make no negative
  decision; the trust chain reuses context.channelPrompt and the admission
  and restore are extracted into shared functions.
- Stated boundaries: non-owner daemons, daemons with different global
  qwen dirs over one runtime base, a second-writer restore of a session
  another process writes live (as with the channel today), the 7-day
  deletion horizon, once-per-name delivery after a rename.
2026-11-06 01:41:35 +08:00
Shaojin Wen ad487ffedb feat(autofix-pr): give helper refusals a code and a next step (#16238)
* feat(autofix-pr): give helper refusals a code and a next step

A helper that refuses now exits 1 with one JSON line last on stderr —
{"refused":"<code>","next":"<step>","message":"…"} — instead of throwing
to the CLI's critical-error handler, which printed the error's stack. Each
yargs handler catches the error and prints the line, as review's
extract-step does. Anything that is not a coded refusal is `failed`.

The step depends on the helper as well as the code: `watched_elsewhere` is
`take_over` from `prepare` and `end_task` from a round's helper,
`worktree_dirty` is `discard` from `sync` and `needs_human` from `push`,
and a `reply` that failed is `retry`, as its second run skips what the
first one posted.

The skill acts on `next` through a new "Refusals" section instead of
matching the wording of messages, and the autofix_pr tool returns the coded
line first, verbatim, then what the helper printed before it. Design
section 6.16 lists the codes.

A clone with no directory for the watch's lock (its watch files removed)
is refused as `not_prepared` instead of with ENOENT.

* fix(autofix-pr): code the refusals whose step the caller needs

- A reply plan that is not JSON or not shaped as a plan is `plan_invalid`
  (`fix_plan`), not `failed` (`retry`): retrying it unchanged fails again.
- A switch back that `sync --discard` asked for and git refused is
  `failed`: as `worktree_dirty` it answered `discard`, which asks again.
- The skill's Resume step keys on the code `pr_closed`, since `prepare`
  gives it `report`; `retry` holds wherever a step says a refusal ends it;
  refusals with no coded line are the calling step's to handle.
- The tool takes the last coded line of stderr, not the last line, and a
  refusal's message keeps at most 8,000 characters, its head and tail.
- Each helper's code is pinned through the command in an empty clone.

* fix(autofix-pr): stop a watch git cannot switch back, and fit the coded line

- `sync --discard` that git cannot switch back to the watch's branch
  answers `synced: false`, as a worktree still dirty after discarding
  does, so the round stops instead of reporting the same refusal at every
  fire.
- A reply plan that cannot be read is `plan_invalid` as well.
- A refusal's message drops terminal controls and is cut to 8,000
  characters as JSON, not as text, so the line fits the stderr the tool
  keeps.
- The tool reads a coded line only from a helper that exited 1, and only
  when nothing but Node's warnings follows it.
- The skill's `discard` step tells Round step 1, where it clears what an
  earlier round left, from the push of step 6, where the round's own fix
  left the worktree unclean.

* fix(autofix-pr): answer the first review of the refusal codes

- A switch back git refuses because the branch is checked out in another
  worktree names that worktree and how to free it: `cleanup` cannot
  delete the branch either while it is held.
- A message drops escape sequences whole and the daemon's log-unsafe
  class (C1 controls, bidi overrides, invisible formatters), keeping tabs
  and newlines.
- A defect, not a refusal, prints its stack frames before the coded line.
- The coded line survives the frames `--trace-warnings` adds after it.
- `fix-start` refuses a fix in flight with the message that says how it
  ends.
- The tool's description states the result's prefix, and `partial` is
  described by the deadline that raises it.
- Tests: the whole step matrix as a specification, `cleanup` and `status`
  through the command, the order of the watch checks, prepare's `failed`
  arm, the line within the stderr tail the tool keeps, and the skill's
  steps read from the helpers' source with what each does.

* test(autofix-pr): pin a C1 control a refusal's message drops

* fix(autofix-pr): say how to free a branch a stale or locked worktree holds

When `sync --discard` cannot switch back because another worktree has the
watch's branch, the reason now fits what holds it:

- a worktree whose directory or `.git` was deleted by hand: drop its
  record with `git worktree prune`, since `git worktree remove` refuses
  one whose `.git` is gone;
- a locked worktree: unlock it first, since neither `remove` nor `prune`
  touches a locked one.

The test runs each case's git commands as the reason gives them and
checks that the next round gets through.

The every-helper refusal test now joins its fake `gh` to PATH with
`path.delimiter`, and its `prepare` row refuses before calling gh. The
fake records its calls and the test asserts there are none, because on
Windows `execFile` would never run the script.

* fix(autofix-pr): quote the holder path in the recovery commands

The `git worktree remove` and `git worktree unlock` commands that a
switch-back refusal prints interpolated the holder's path bare. A path
with a space split into two arguments (git exits 129), and a quote broke
the shell, so the branch stayed held. Both commands now quote the path
with `shellQuotePath`, the form review's printed repair commands use.

The holder test now runs every case at a plain path and at one with
spaces and both quotes. It runs the printed commands through `sh -c`
instead of splitting them on spaces.

* fix(autofix-pr): answer the second review of the refusal codes

- The tool runs its helpers with NODE_NO_WARNINGS=1. Node flushes a
  warning raised while a helper refuses after the coded line, and a
  warning's text can run over several lines, which hid the line from
  `codedRefusal`, so the code and its `next` were lost. A new test runs a
  helper that raises such a warning: run directly, the helper prints the
  warning after the line; run through the tool, its line is read.
- The switch-back reason's git commands carry `-C` with the watch's
  worktree, so they run as printed from any directory. A branch held by
  the clone's own checkout gets only "switch that checkout", because git
  never removes the main worktree.
- `worktreeHolding` reads `git worktree list --porcelain -z` (git 2.36+)
  and falls back to the line form on older gits, so a path with a
  newline in it is no longer cut short.
- New tests: the holder matrix also runs at a path with a newline and
  runs its commands from outside the clone; the main-checkout holder;
  and the no-holder arm (an index.lock stops the switch), whose `cleanup
  --force` advice nothing reached before.

* fix(autofix-pr): keep a watch whose branch another checkout has

When `sync --discard` cannot switch back because another checkout has
the watch's branch, it answers `synced: false`, and the skill stops the
watch with `cleanup`. `cleanup` removed the worktree first and then ran
`git branch -D`, which git refuses while that checkout has the branch.
The watch was left with its state file and branch and no worktree, and
every later `cleanup` and `prepare` failed at the same delete.

`cleanup` now checks `git worktree list` before it removes anything,
`--force` or not. When another checkout has the branch, it keeps all
three and ends the watch, as it does for unsaved work. It answers
`removed: false` with `heldBy`, and a reason that says how to free the
branch and to run `cleanup` again. The holder lookup is shared with
`sync` (`branchHolder`) and anchors its commands at the clone root.

The skill's Stop step and both design docs say what `heldBy` means.
2026-11-05 16:53:30 +00:00
11f6163f0f perf(managed-agent): narrow SSH settlement and worktree locking (#16386)
Settle SSH registrations using their operation and exact physical owner.
Allow ordinary Runtime members and uploads during main worktree preparation
while retaining maintenance ownership and cleanup exclusion.

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

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

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

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

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

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

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

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

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

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

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

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

Deleting the shared stub's fail arm, its empty-stdout arm or its argv
capture turns the case red, and so does recording only the first
argument.
2026-11-05 23:50:44 +08:00
Shaojin Wen ef6914c1d9 fix(channels): keep GitHub replies hit by a secondary rate limit (#16334)
* fix(channels): keep GitHub replies hit by a secondary rate limit

A final reply that GitHub rejected with a secondary rate limit was
classified as an ambiguous delivery failure: it was not retried, not kept
as a pending delivery, and no error comment was posted, so the answer was
lost while the notification was marked read. GitHub sends secondary
limits with quota left (and not always with retry-after), so only the
exhausted-budget shape counted as a definite no-write.

Treat a 403/429 that carries GitHub's rate-limit headers and names a
secondary limit as a definite no-write too. The reply is retried in
process and, if the limit persists, kept as a pending delivery for the
next channel start. Responses without GitHub's rate-limit headers (for
example a 429 from a proxy) and permission 403s stay single-shot.

* fix(channels): wait out GitHub secondary limits before retrying

A secondary rate limit without retry-after fell through to the generic
1 s / 2 s backoff, so the final reply's three attempts landed within about
three seconds while GitHub was still throttling the account; GitHub asks
for at least a minute in that case, longer on each repeat, and warns that
continued requests can get an integration banned. Wait 60 s and then 120 s
for such a limit; retry-after and the budget reset still take precedence.
Note in the classifier why a retry-after header alone is not proof of a
rejected write there.

* fix(channels): stop the pending GitHub retry pass on a rate limit

Now that a secondary limit counts as a definite no-write, the retry pass
on channel start spent three attempts on every pending reply while the
same account-wide limit answered each of them; stop the pass at the first
rate-limited record and keep the rest for the next start. Pin the retry
count and waits per secondary-limit shape, the older abuse-detection
wording, and that a 502 carrying an exhausted-budget header stays
single-shot. Reword the classifier comment: the header shows a rate
limiter answered, and GitHub's wording tells which one.

* fix(channels): end GitHub rate-limit waits on stop, keep cleaning pending replies

- disconnect() used to cut a rate-limit wait short and let the remaining
  attempts go out back to back into the same limit. A stopped wait now gives
  up with GitHub's last answer, so a final reply stays a definite no-write
  and is kept as a pending delivery.
- The pending pass stops sending at the first record GitHub still
  rate-limits (either limit) but keeps dropping later records the audit
  already shows as posted, which needs no request.
- The PR watch pass treats a 403 carrying GitHub's secondary-limit wording
  as shared, like the publication and notification paths.
- A retry-after that is not a positive number of seconds (an HTTP date,
  "0") no longer skips the secondary-limit minimum.

* fix(channels): treat retry-after on a GitHub-answered 403/429 as a rate limit

The final reply's definite no-write required an exhausted budget or GitHub's
secondary-limit wording, so a rate-limit rejection that carried GitHub's
x-ratelimit-remaining and retry-after but worded its body differently was
audited ambiguous and the reply was dropped. retry-after is GitHub's own
documented signal for that limit; with the x-ratelimit-remaining header still
required, a proxy's bare 429 stays single-shot. A 403/429 whose retry-after is
unusable now waits the secondary-limit minute as well.

Also pin the shape where GitHub rejects the reply once and accepts the retry.

* fix(channels): stop GitHub retries by the error's shape, not the wait branch

- The stop guard keyed on which wait was chosen, so an exhausted budget
  without a reset header took the generic 1 s / 2 s branch and, after
  disconnect(), still fired its remaining attempts into the spent budget.
  Rate-limit answers are now recognised by their shape (403/429 with an
  exhausted budget, retry-after or the secondary wording) for the wait, the
  stop guard and the definite no-write check alike; such an answer without a
  usable retry-after or reset waits the one-minute minimum.
- x-ratelimit-remaining must be a count: an empty value no longer reads as
  an exhausted budget and turns a permission 403 into a stored retry.
- The pending pass starts no further record once a stop is requested, even
  while another write in flight defers its abort.
- Tests pin the stop guard for every wait, reconnecting, a kept record
  between the stop and a posted one, and an infinite retry-after.

* fix(channels): let an answer that lands after a stop decide the pending reply

- The start-up pass returned on an aborted signal before looking at the
  error. A stop defers the abort until the in-flight write ends, so a 502 or
  network failure from that write (GitHub may have posted the comment) left
  the record and its reply_pending task for the next start, which posted the
  reply again. The pass now classifies first; only an attempt that was never
  sent leaves its record, and githubApi marks that abort as not sent.
- A rate limit without GitHub's header (a 429 with only retry-after) is
  still dropped as ambiguous, but it now stops the pass like a definite one
  instead of spending every later record against the same limit.
- x-ratelimit-remaining as a number must be a non-negative integer, and the
  watch pass judges a 403 with the same rate-limit rule as the reply.
- A call that starts after the stop gives up before logging or sleeping.

* fix(channels): treat a past GitHub budget reset as clock skew

GitHub answering with an exhausted budget means its window has not reset, so
a reset time that already looks past says the local clock is ahead; the wait
fell to a 1 s retry and spent all three attempts in about two seconds. It now
waits the one-minute rate-limit minimum instead. Tests give the secondary
limit GitHub's usual reset header and pin the start-up pass keeping its
records when a stop, deferred by another write, lets the wait run out.

* test(channels): pin the ambiguous audit, the watch 429 abort and the quiet stop

Lock three behaviours the suite let drift: a header-less rate limit on the
final reply is audited failed_ambiguous (the key stays consumed), a bare 429
aborts the whole PR watch pass, and a reply that starts after the stop logs
no retry it will not make.

* docs(channels): name the header a GitHub no-write answer needs

The no-write rule needs a numeric x-ratelimit-remaining on the 403 or
429, so a rate-limit answer without it (a 429 with only retry-after,
say) is ambiguous and its reply is not kept; the guide now says so. The
publication test helper's comment also said the opposite of what
stopping the poll loop does to retry waits.

* docs(channels): bring the GitHub publication contract up to the retry rules

The contract still said a definite no-write goes straight to the pending
file. It now names what counts as one, the two in-process retries and
their waits, what an ambiguous answer to a retry does, the stop that
keeps the reply pending, and how the start-up pass stops at the first
reply GitHub still rate-limits. The first bullet now speaks of one final
response rather than one delivery attempt.

* docs(channels): add the Chinese version of the GitHub publication contract

docs/design/README.md asks a change to a single-language design to add
the missing version. The publication contract gains its zh-CN twin,
reciprocal links under both titles, and a Verification line for the
retry tests.
2026-11-05 22:37:41 +08:00
Shaojin Wen 6611b8aeb2 fix(cli): land a Web Shell update on the current latest release (#16374)
* fix(cli): land a Web Shell update on the current latest release

The daemon pins its update target when it prepares. Once the state is
`ready` the check returns the cached status even for `?refresh=true`, so
polling never re-downloads — and Web Shell prepares automatically on
mount. For a long-lived daemon the prepared release is therefore whatever
was latest shortly after it started. Clicking Update restarted into that
stale target, and every release published in between needed another
prepare-and-restart round, which reads as advancing one version per
click. The CLI resolves and installs at each process start, so it always
reached the current latest in one step.

Re-resolve the release once at activation, after the restart has been
acknowledged, and stage the newer one when it is higher and one-click
covers the installation. Both the standalone and the managed npm paths
share one resolver, so eligibility cannot drift between the check and the
click. Readiness stays pinned and background polling still never
re-downloads; only the explicit click re-resolves.

A failed re-resolve or staging applies the prepared release instead, so a
registry failure cannot strand an explicit update request on the running
version. The activation guard still preserves an equal or newer active
installation, and a failed activation of the newer release cleans both
payloads.

Staging runs after the acknowledgement and can take minutes, during which
the daemon keeps answering. The browser now measures its give-up deadline
from continuous unreachability rather than from the click: a reachable
daemon is still working, and the old click-time budget would have
reported a failure while the update was in fact succeeding.

The protocol reference and both language versions of the update design
doc state the pinned-readiness contract and the new deadline semantics.

* fix(cli): keep an update restart observable and its payload committed

An audit of the activation-time re-resolve found three defects in it.

Discarding the superseded payload ran before the newer one was committed,
so a failed discard fell back to a payload that might already be half
removed and orphaned the one just staged, losing the update entirely.
Commit to the newer payload first and discard the old one best-effort: a
failed discard now leaks disk instead of losing the update.

Dropping the browser's click-time deadline had also removed its only
overall bound. A daemon whose process replacement does not land on the new
build comes back reachable and settled on the same version, and the
control then waited forever for a version change that would never arrive.
Restarting is sticky until the process is replaced, so an acknowledged
restart followed by any other state with an unchanged running version now
terminates the wait and reports a failure. That path publishes the settled
state itself, because the poll immediately after it can drop while the
daemon is still coming back, which would otherwise leave a stale
restarting status rendering no control at all. The rule is gated on the
restart having been acknowledged: polling starts on the click, before the
request resolves, and can still observe the pre-click state.

Browser fixtures now model the daemon's contract — restarting from the
acknowledged restart until the process is replaced — because a fixture
that keeps reporting a prepared state after the click cannot exercise that
distinction. Assertions are unchanged; only the stimulus became faithful.

Also pin the edges the re-resolve depends on: a prerelease below the
prepared release, a registry rollback, build metadata that does not change
precedence and a malformed registry value must none of them restage, and
an in-flight restart must survive the operator disabling automatic
updates, concurrent polls and retries, and ordinary shutdown. Every
mechanism added here is mutation-witnessed, including the two halves of
the deadline change and the acknowledgement gate.

* fix(cli): sample the restart acknowledgement when the poll starts

The settled-state termination read the acknowledgement ref after awaiting
the status request, which does not establish that the response was sampled
after the acknowledgement. The status and restart requests are independent,
so the first restart poll can be issued while the restart is still pending
and deliver its pre-restart snapshot after the 202 arrives. The ref is then
true, the stale snapshot looks like a daemon that settled without updating,
and the control reports a false failure and clears the restart — after which
a genuine version change is handled by the idle branch, so the page never
reloads and the captured task URL is never restored.

Sample the flag when the poll is initiated instead. A response delivered
after the acknowledgement by a poll started before it is now ignored, and
the next poll — initiated afterwards — is the first one allowed to conclude
the replacement did not land. The regression test holds the status request
open, acknowledges the restart, then delivers the old snapshot, and requires
no error plus a successful reload once the new version is reported.

Also restore an absolute backstop, which the move to a continuous-
unreachability deadline had removed. A daemon that stays reachable but never
leaves restarting — a hung activation or process replacement — would
otherwise leave the control disabled forever with no error, and a page
reload hides it entirely. The cap sits above the daemon's own staging
ceilings (five-minute archive download, ten-minute managed npm child) so a
slow but progressing update never reaches it, and a test pins both halves:
the wait survives fourteen minutes of a reachable restarting daemon and ends
at the cap.

Both the protocol reference and the two language versions of the design doc
state the sampling order and the backstop.

* fix(cli): log why a click fell back, and keep the entry after giving up

Three review findings on the update restart path.

A registry outage at click time left no trace. The release check resolves
an error result rather than throwing, so the fallback returned silently and
"the registry said nothing newer" was indistinguishable from "the registry
was unreachable" — both install the older prepared release. Log the
classified failure before falling back. The catch below only ever sees
detection or staging throw, so it now names restaging instead of sending the
reader to the registry. The test that covered this used a rejected check
promise, a shape the real function does not produce for an outage; it now
resolves the real error result and asserts the log, and a second test pins
that a staging failure is not reported as a registry failure.

Giving up took the update entry with it. While the wait runs, each
successful poll publishes the daemon's state, so a wedged daemon leaves it at
restarting; the give-up branch cleared the restart without publishing
anything, and the render gate does not display restarting, so the entry
unmounted and could not return while the daemon kept reporting it. The user
got one notification and then no affordance at all. Every give-up path — both
deadlines and a daemon-reported error — now keeps a retryable entry on screen.
The design sentence claiming the control is never hidden was untrue of these
paths; it is now true of all of them. The wedged-daemon test asserted only
that the spinner was gone, which a vanished control also satisfies, so it now
asserts a visible enabled entry.

The absolute cap was sized against a ceiling the code does not have. The
standalone archive download tries OSS and then GitHub sequentially with a
five-minute budget each, and the checksum and signature fetches add up to two
more minutes, so one restage can spend about twelve minutes downloading and
verifying before activation — not the five the docs and the constant's
comment claimed. That left the fifteen-minute cap about three minutes of
headroom while the text promised a slow update would never reach it. Raise
the cap to twenty minutes and state the real chain in the protocol reference,
both design docs and the comment.
2026-11-05 22:29:12 +08:00
Shaojin WenandQwen-Coder edb094fea3 test(managed-agent): gate lifecycle-v2 close with a stopped worker (#16123)
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-05 22:22:26 +08:00