Commit Graph
11879 Commits
Author SHA1 Message Date
易良andzzj 0f22726809 fix(core): honor Anthropic transport timeout configuration (#16193)
Co-authored-by: zzj <[email protected]>
2026-11-03 17:58:22 +08:00
qqqys d7f534edd6 ci(e2e): run the nightly suite with the unified agent loop on (#16192)
The nightly and on-demand E2E runs gain a Linux sandbox:none leg with
QWEN_CODE_ENABLE_UNIFIED_AGENT_LOOP=1, so the whole integration suite
also runs its headless and ACP sessions on the shared loop. This is the
scheduled job a surface's default change waits on. Pushes keep their
two legs.

The setting's description now says that headless and ACP sessions use
the shared loop, and that the interactive TUI and subagents keep their
own for now. The design doc describes the job in place of one that did
not exist.

Part of #15327.
2026-11-03 17:57:53 +08:00
qqqys 68fb173041 feat(acp): run cron and background-notification turns on the shared loop (#16190)
* feat(acp): run cron and background-notification turns on the shared loop

With experimental.unifiedAgentLoop on, ACP's cron turns (loop ticks
included) run as Cron rounds and its background-notification turns as
Notification rounds on the shared round, the shared tool batch and the
client's stop point, with the Todo Stop Guard as their stop policy.

The prompt loop's shared round moves into #runSharedRound, which the
prompt, cron and notification loops share; the prompt loop passes in
what only a prompt has. ACP keeps the scheduling around these turns,
records the cron prompt once it reached history and the notification
with its task, and shows each notification response as one background
message.

userTurnRecordedByHost also covers Cron and Notification sends, whose
record the host writes.

The ACP parity suite gains a background-shell scenario whose result
starts a notification turn.

Part of #15327.

* fix(acp): settle shared cron and notification turns as the current path does

A cron turn that loop protection or a declined approval stopped is no
longer delivered to its channel, as the current path returns before
delivery. The cron prompt is recorded as the first event of its response
arrives, before the client records the response, so the record keeps the
prompt ahead of its answer. A cron or notification round that threw on
its aborted signal, which only the session aborts, ends as cancelled
rather than firing StopFailure. The cron turn counts its rounds as they
start, so a round that throws still counts.

The automatic loop emits a response's usage through the shared round's
emitResponseUsage instead of repeating it, and the shared round
destructures what it aliased.

* docs(design): list the cron prompt a cancel before its first request keeps
2026-11-03 09:52:03 +00:00
1b689dd4be feat(serve): enable remote access from settings without restarting (#15887)
* feat(serve): enable remote access from settings without restarting

Add a locally managed, independently authenticated listener with live revocation and session continuity. Explain connection setup and reflect daemon availability in the sidebar.

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

* test(web-shell): expose isDaemonMixedContent in the boot-test daemon mock

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

* fix(serve): scope the Local Control pairing secret to its own listeners

`GET /workspace/local-control` returned the unredacted pairing URL and QR to
any bearer-authenticated request. The secondary listeners serve this same
Express app and each verifies only its own token, so a remote-access client
reached the Local Control pairing credential — a secret belonging to a
different listener, which disabling remote access never revokes.

Gate the unredacted branch on the listener that owns the credential (primary
or local-control) as well as on operator authority, and pin both arms: a
remote-access bearer gets `urlRedacted: true`, the owning listener still gets
the URL.

`POST /workspace/local-control/disable` stays open to remote-access callers.
That is now a stated decision rather than an inherited default: the bearer
already reaches every other route on this app, so denying it here would be
security theatre while breaking remote management, and the response is
redacted by the gate above.

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

* fix(cli): tear down a permission-blocked prompt at disconnect grace expiry

The preserve-running-prompts branch returned early for any live promptAbort,
including a prompt blocked on a client permission vote registered while the
stream was live. Once the remote entrance is revoked the vote can never
arrive (permissionResponseTimeoutMs defaults to 0), so the turn never
settled, promptSettled never ran, hasRunningDetachedPrompt stayed true, and
sweep plus both connection reapers skipped the connection forever — pinning
an ownedSessions entry and one of the maxConnections slots until restart.

Gate the preserve branch on hasPendingClientRequest: a session with an
outstanding permission vote falls through to the existing teardown, which
abandons the pending request and detaches the bridge client. A prompt that
is not waiting on the client is still preserved unchanged.

Witness: the new regression test fails without the condition (abandon
callback never fires: [] vs ['bridge-req-1']) and passes with it; the
existing 31-minute preserve test stays green (65/65).

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-11-03 17:16:50 +08:00
bc19325983 feat(web-shell): add host mount entries and align managed empty-state layout (#15928)
* feat(web-shell): 增加 managed-host 构建入口与托管会话页

为产品侧集成提供独立的 managed-host 产物(--mode managed-host)与托管会话页:
不挂 daemon workspace/session context,托管会话由 provider 直接渲染列表、会话与输入框。

包含:vite lib 构建模式、managed-host 渲染入口及测试、ManagedSessionsPage 托管分支与
测试、provider 类型调整、README 与中英文设计说明。本次不涉及宿主注入能力。

* feat(web-shell): 托管宿主可注入侧栏导航 props

ManagedAgentWebShell 增加可选导航 props 并原样透传给 ManagedSessionsPage:
navigationTarget / archivedNavigationTarget / onArchivedSessionsStatusChange /
navigationActive / navigationSearch。

宿主据此决定是否把官方会话列表 portal 到自己的侧栏(navigationTarget 为 null 时
仍走 home 布局但不 portal),并同步搜索词与归档计数。类型复用 ManagedSessionsPage
现有 props,不新增重复定义;新增 2 个用例覆盖原样透传与未传时保持 undefined。

* feat(web-shell): add full-page app-host entry and align managed empty-state layout

- Add app-host mount entry (renderWebShellApp) so a non-React host can render the
  complete WebShell (sidebar + welcome + composer) by injecting baseUrl/provider;
  no shell is hand-drawn and no backend address is hardcoded.
- Center the managed empty state like the standalone chat (content shrinks so the
  chatPane centers it) instead of pinning the welcome to the top and composer to
  the bottom; gate the welcome header off when a failed session is selected.
- Surface session failure and unconfirmed-submission notices together above the
  composer, dedupe the same message across channels, and style them as callouts.
- A pending prompt owned by another session now offers a redirect to the owning
  conversation instead of retry/discard from a foreign session; reconcile tests.
- Build app-host with global (unscoped) CSS so root-level state classes match.

* test(web-shell): align managed status smoke assertions

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

* test(web-shell): reach the owning conversation before retrying a foreign pending

A pending prompt owned by another conversation no longer offers retry in the
foreign session: it renders "Open the original conversation" instead. The
spec still clicked "Retry the same request" straight from the foreign session,
so the two foreign variants timed out waiting for a button that is deliberately
absent from this head while the owner variant passed.

Follow the redirect first, then retry. Owner variant unchanged.

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

* docs(web-shell): document the app-host entry and drop ignored e2e links

The review on head 932a61d8dc read `app-host` as undocumented: no README
section, no mention in either design doc. Document it the same way
`managed-host` is documented -- entry API, build mode, served directory --
and record the unscoped-CSS behaviour as a deliberate decision narrowed to
that bundle, naming the entries that keep scoped CSS. Also state that neither
host mode is part of `npm run build`, which is what the wiring actually does.

Both design docs linked `.qwen/e2e-tests/managed-host-webshell.md`, which
`.gitignore` excludes (`.qwen/*`), so the links are dropped and the
statement they supported stays.

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

* fix(web-shell): revert breaking changes to existing consumers

- Revert `isChatEmptyState` to `!managedMode &&` (was: `!connection.sessionId && !(managedAgentProvider !== undefined && managedSessionId)`), restoring the original behavior where managed mode is never empty state. The head condition broke existing managed consumers by making them hit empty-state layout when no session was selected.

- Revert `sidebarRailEnabled` to drop `managedAgentProvider === undefined &&`, restoring rail for existing managed consumers. The condition was app-host-specific and should not affect hosts that already pass a provider.

- Revert `ManagedSessionsPage` inline `flex: 0 0 auto` (was unconditional in empty state), restoring `flex: 1 1 auto` for the managed entry. The shrink was only correct when paired with `.appChatEmpty .chatPane { justify-content: center }`, which only exists in the full-page `renderWebShellApp` entry. In `renderManagedAgentWebShell` the class is absent, so the shrink alone produced a 136px content block pinned to the top with 550px dead space below.

- Remove `skipScope` from `vite.lib.config.ts`: the justification ("scoped selectors use [data-web-shell-root] as an ancestor anchor") is factually incorrect. `COMPONENT_SCOPE` includes `:where([data-web-shell-root][data-web-shell-shadcn], ...)` which matches the element itself, so scoped bundles already apply state classes on the root element. Keep scoping for all modes.

- Update README and both design docs to reflect the corrected scoping behavior.

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

* chore(web-shell): remove app-host entry

The app-host entry (renderWebShellApp) had no build wiring (absent from
build script, exports, files, and no CI job invoked --mode app-host), no
test coverage, and hardcoded a specific host's sidebar/footer configuration.
Remove it from this PR; it can be reintroduced in a follow-up with proper
packaging and tests once a named consumer exists.

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

* fix(web-shell): drop unused mode parameter from injectCssModules

The mode parameter became unused after the skipScope removal. Remove it to
fix the ESLint no-unused-vars error.

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

* docs(web-shell): scope the managed-host entry and correct stale layout notes

- README: say plainly that `managed-host` is source-only (no `build`,
  `exports` or `files` wiring, so it is not in the npm package), and record
  that one shell per shadow root is the contract.
- package.json: keep the orphan `dist/types/managed-host.d.ts` out of the
  published tree so the package does not ship a type with no implementation.
- ManagedSessionsPage: point the `summary.failure` dedupe comment at the
  current failure block above the composer, and record why a session switch
  resets failure and discard notices.

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

* docs(web-shell): state the real consequence of the session-switch reset

The comment claimed the list error is re-derived on a session switch. It is
not: the list effect's deps exclude sessionId, so a catalog-load error is
hidden until provider/listRevision changes. Say what actually happens.

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

* fix(web-shell): keep managed sessions visible when workspaces collapse

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

* fix(web-shell): address doudouOUC review feedback

- Remove shadow-DOM style adoption from renderManagedAgentWebShell: the
  shared CSSStyleSheet cache caused cross-contamination when two shells
  shared one shadow root. Hosts using shadow DOM should adopt styles
  themselves or use a plain container.
- Scope the error/discard reset effect to hostSessionOnly only: the
  unconditional clear on [provider, sessionId] hid the catalog-load error
  for existing managed consumers on every session switch without retrying.
- Update the managed-host test to match the removed shadow-DOM behavior.
- Drop the stale shadow-root note from README.

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

* fix(web-shell): preserve styles for managed hosts in shared shadow roots

Restore the documented managed-host ShadowRoot style lifecycle and keep the shared package sheet until its last owner unmounts. Cover both fallback and constructable sheet behavior.

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

* fix(web-shell): center the managed host empty conversation as one group

The host mount entry has no .appChatEmpty ancestor, so the managed empty conversation kept its full-height content box and pinned the composer to the bottom. Give host-mounted empty conversations the same shrink-to-content centering, mount the entry through the managedHome fill wrapper, and drop the empty trailing status row below the composer.

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

* fix(web-shell): let a host-mode session clear through without an echo channel

hostSessionOnly alone marks the shell controlled, so the page's clear
signal after deleting or archiving the current session was dropped when
the host passed no onSessionChange (the README-documented mount). The
deleted conversation stayed on screen and sendable.

Allow the internal write for a clear (undefined) when no echo channel
exists; new selections stay host-owned, preserving the existing
'unrequested selection is suppressed' contract. The bot's suggested
controlled-expression rewrite would have broken that contract, so the
gate lives in the handler instead.

Witness: the new no-echo test fails at 5683eed0 without this change
(sessionId stays 's1') and passes with it; the echo-channel test pins
that the fix does not leak internal writes into echo-owned hosts.

---------

Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-03 16:50:24 +08:00
易良andQwen-Coder 1f09497f45 fix(core): require explicit trust for project frontmatter and LSP (#16182)
* fix(core): require explicit trust for repository execution

* test(integration): explicitly trust skill hook fixtures

* test(lsp): add isExplicitlyTrustedFolder to document-sync mock config

The document-sync tests create a bare Config mock with only
isTrustedFolder. The new explicit-trust gate in NativeLspService and
LspServerManager calls isExplicitlyTrustedFolder(), which is undefined
on the mock, producing TypeError at reload time.

Add isExplicitlyTrustedFolder: () => true alongside the existing
isTrustedFolder mock so the 13 affected cases pass again.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-03 15:44:31 +08:00
ytahdnand钉萁 3a4c7dd0e9 fix(web-shell): handle image URL pastes and unavailable files (#16179)
* fix(web-shell): preserve images pasted with source URLs

Keep editable office text first and warn when advertised clipboard files are unavailable.

* fix(web-shell): keep command URL pastes as text

---------

Co-authored-by: 钉萁 <[email protected]>
2026-11-03 15:14:12 +08:00
qqqys 6993385a63 feat(mods): run prompt.submit hooks in the Ink terminal (#16180)
* feat(mods): run prompt.submit hooks in the Ink terminal

Mods' prompt.submit hooks now hear each prompt typed in the Ink terminal before the input history, the transcript row, @ references and UserPromptSubmit read it. A rewrite stands everywhere the typed text would, with model-only notices kept ahead of it, and context joins the hidden hook context block. A dropped prompt shows its reason, stays out of the transcript and the input history, and comes back to an empty input box; so does a prompt cancelled while its hooks run. Only the Ink terminal opts in, so OpenTUI keeps skipping these hooks.

* fix(mods): keep pastes, attachments and steering behind prompt.submit in Ink

Hold queued prompts for the end of the turn while a Mod hooks prompt.submit, so steering at a tool boundary cannot bypass the hooks. Find the typed text in the model prompt with its pastes framed, keeping attachment references and notices ahead of a rewrite and re-collapsing pastes the rewrite keeps whole. Treat a whitespace-only difference as no rewrite. Hand a dropped or cancelled prompt back with its paste placeholders, ahead of anything typed since.
2026-11-03 14:45:35 +08:00
易良andQwen-Coder 31f64eb2e9 test(cli): pin same-turn shell notification delivery (#16176)
* test(cli): pin same-turn shell notification delivery

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

* style(cli): format the same-turn shell delivery test

Lint & Static failed on this head inside the incremental prettier step,
naming only Session.test.ts. Reflow the new `notifyShell` cast with the
repository's own prettier (3.6.1, matching the ^3.5.3 pin); the hunk is
whitespace only, ten lines re-wrapped, no assertion or call changed.

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

* test(cli): assert same-turn shell active-work hold

Shell holds are reported by category rather than by shell id. Move the check ahead of the delivery assertions so removing the consumed notification leaves the worker active and fails at the assertion meant to witness it.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-03 14:41:58 +08:00
Shaojin WenandQwen-Coder 68c9fa27fb feat(verify-pr): add local Docker recipe with a host-side model proxy (#16077)
* feat(verify-pr): add local Docker recipe with a host-side model proxy

A local /verify-pr round needs the same isolation the CI lane provides,
but the skill only documented the boundary check, not how to run the
model credential through it. Add scripts/verify-pr-model-proxy.mjs, a
host-side relay that holds the real key and lets the container call the
model with a per-run bearer token, mirroring the CI lane's inline proxy
(POST /chat/completions only, nonce health check, per-chunk idle
watchdog). The listen address is a flag so the proxy can bind the Docker
bridge, and readiness is printed as one JSON line.

The SKILL recipe pairs it with a docker run template: the token travels
as QWEN_VERIFY_PROXY_TOKEN (not OPENAI_API_KEY, which the isolation gate
refuses), and the container's provider entry reads it via envKey. The CI
lane keeps its inline copy — the proxy holds the real key, so it must
not execute a script from the PR-head checkout.

Verified: 11 new pin tests plus all existing verify-pr suites (120
passed), and a real docker smoke — the gate passes inside
node:22-bookworm with the proxy token, refuses OPENAI_API_KEY with exit
4, and a container POST reaches a mock upstream with the real key
injected and the /v1 prefix preserved.

* fix(verify-pr): harden the model proxy after adversarial audit

Audit round 1 found the relay forwarded client hop-by-hop headers
(connection, transfer-encoding, expect, proxy-authorization, ...) to the
upstream, which RFC 7230 forbids and which fights undici's own framing
for the relayed stream. Strip them, with a chunked-body test that pins
the behaviour. Round 1 also found the CLI entry had no coverage: add
tests for the missing-config exit code and the printed JSON coordinates
(token override, base-path prefix, no real-key leak, health nonce), and
document that the printed baseUrl uses the listen address, so binding
0.0.0.0 yields a URL clients cannot dial.

Rounds 2 and 3 (open-ended re-read plus adversarial passes over the
updated diff) found nothing further. All verify-pr suites: 123 passed.

* fix(verify-pr): address review findings in the model proxy and checker

Round-1 review (12 inline findings at 70aed891bf). Proxy: abort the
upstream when the client disconnects before headers instead of paying
for a completion nobody reads (R1-1); make the idle watchdog pause-aware,
re-arm on drain, and match the client's own 240 s stream tolerance
(R1-3); bracket IPv6 listen hosts via an exported formatter and normalize
base paths once (R1-12). Checker: add --context so a host-held snapshot
pins the expected head instead of the writable artifact copy, and refuse
an unusable snapshot rather than silently falling back (R1-9).

Each guard is pinned by a test that was verified to go red when the guard
is removed (six mutations: pre-header abort, pause guard, per-chunk
re-arm, body error handler, IPv6 bracket, --context).

* docs(verify-pr): harden the local container recipe and record the design

Recipe: run the key-holding proxy and the checker from a trusted
checkout, never the PR tree (R1-6, R1-9); background the proxy with a pid
file and stop it at the end (R1-4); read the key instead of inlining it
(R1-5); use a named detached container driven by docker exec (R1-7);
require a standalone clone, the host uid and a fresh HOME (R1-8); mount
the full host-held metadata snapshot rather than the four-key artifact
copy (R1-10); and attest only inside, after rejecting credential-shaped
ignored files (R1-11). Add the bilingual design record (R1-2), cross-link
it from the local-round design, and pin the recipe's trust lines in tests
so reverting them turns the suite red.

* fix(verify-pr): address round-2 verification findings

Round 2 at 3509c2d1aa reported one Low and four Suggestions.

- Relay the exact <basePath>/chat/completions path. A suffix match also
  forwarded /admin/chat/completions to the upstream.
- Pin the response content-encoding strip with a gzip upstream: undici
  hands the proxy decoded bytes, so forwarding the header makes the client
  gunzip plain text.
- Reject credential-shaped ignored entries at any depth (a nested
  secrets/.env included) instead of only a root .env/.env.*.
- Name the round's engine as the released CLI and offer the tree build for
  a round that must exercise the PR's own CLI.

The CI inline copies keep their existing two-header strip: that lane is
untouched by design and a change there needs its own workflow review.

Tests: 141 passed. Three mutations verified red (exact path, gzip header,
nested credential pathspec). The review-pr check at this PR is red because
the bot lane exhausted ten retries on "429 rpm exceeded", not because of
this diff.

* fix(verify-pr): close round-3 trust-boundary findings

Round 3 at 3ccf38697d reported two Criticals against the recipe, one
fix-induced Critical, and four Suggestions.

- prepare, the credential scan, the isolation gate and the checker now all
  come from <trusted-clone>; prepare runs with the PR clone as its cwd so
  the HEAD check still applies (R2-1).
- The base commit's .qwen is extracted and mounted read-only over the
  tree, so the round's instructions and project settings cannot come from
  the PR (R2-2).
- The attestation is passed to the shell that starts the round, because
  every docker exec is a fresh shell (R1-11, fix-induced).
- The proxy token is exported from proxy.json and passed with a bare -e;
  the file is chmod 600 and removed at cleanup (R2-3).
- Connection-nominated hop-by-hop headers are stripped (R2-6).
- Recipe pins now enumerate every checker call site, require a non-empty
  docker-run slice, and seed the writable checker snapshot so a fallback
  regression is caught (R2-4, R2-5).
- New verify-pr-credential-scan.mjs lists ignored files one by one,
  refuses credential-shaped names at any depth, and leaves node_modules
  and dist alone — replacing the pathspec check that refused every
  installed or built tree (round-3 Low).

Tests: 148 passed; ten mutations verified red.

* fix(verify-pr): address round-4 review findings

Four P2 findings at 441a523927:

- The credential scan now limits git enumeration with credential pathspecs
  and depth-aware node_modules/dist excludes, so an installed tree's
  multi-megabyte ignored listing never overflows the output buffer.
- Those excludes apply inside workspace packages too, so packages/*/dist
  and packages/*/node_modules are not mistaken for project credentials.
- The recipe waits for proxy.json to carry a token and validates the
  exported value, so a truncated background write cannot reach Docker.
- The pre-header disconnect test waits until the mock upstream has
  recorded the request before destroying the downstream socket, instead
  of racing the proxy's own (correct) early abort.

Tests: 150 passed; three mutations verified red (enumeration pathspecs,
excludes, readiness loop).

* fix(verify-pr): refuse nested repositories in the credential scan

Round-4 verification at 19b14b1a8f left three Suggestions.

- A nested git repository is now refused instead of silently skipped: git
  does not descend into it, so a .env under tmp/pr-clone/ would pass. A
  vendored .git under node_modules/dist stays excluded like the files there.
- isExcluded is removed: the pathspecs already exclude those directories,
  and the surviving JS filter was unpinnable.
- The spaced Connection-nomination list is pinned; dropping trim made the
  proxy answer 502.
- The node_modules/dist credential exclusions are documented as deliberate.

Tests: 152 passed; two mutations verified red (nested refusal, trim).

* fix(verify-pr): address round-5 review findings

Four Criticals:
- The key entry works without a TTY: an interactive shell prompts, an
  agent's shell reads REVIEW_OPENAI_KEY_FILE and fails with a named error.
- prepare disables fsmonitor, hooks and credential helpers on the
  PR-controlled clone, and the scan refuses credential-bearing remote URLs.
- The proxy token is re-derived from proxy.json in the docker run step,
  because each step is its own shell.
- The credential scan covers .env~, *.p12, id_rsa*, gha-creds-*, and the
  other credential families the repo's own .gitignore names.

Suggestions:
- Guard call site moved to <trusted-clone>, <clone> defined for watch mode.
- The newest verify comment is snapshotted as previous-report.md.
- The real key is unset once the proxy owns it; the token is unset at cleanup.
- `{1..100}` replaces seq for macOS; --user and the proxy token's
  non-credential status are pinned; the scan abort is structural (`|| exit 1`).
- Step 6 wording matches the base-pin; the docs narrow the base-pin scope for
  root AGENTS.md/QWEN.md, list the real CI divergence, explain content-encoding
  in English, fix the zh token wording, and record the checker --context
  contract and the ownership attribution.
- The readiness gate compares the health nonce; --port is removed.

Test gaps closed: gzip header assertion, mid-body disconnect, reset event,
PUT, bare-base exact path, positive header passthrough, 502 transport
failure, full paused-stream equality, token shape, publish --context pin,
checker second positional, scan path fixtures, headers-only idle window.

Tests: 159 passed; twelve mutations verified red.

* fix(verify-pr): close round-5 verification findings

Two Lows reproduced at b5c717a5d5:
- prepare's hardening named clean filters but only disabled fsmonitor,
  hooks and credential helpers. It now enumerates every local
  filter.*.clean/smudge/process and empties it with a -c override before
  any git read; a planted-filter fixture proves the override with a
  positive control that fires without it.
- The nested-repository refusal over-refused installed trees whose
  node_modules holds a real vendored git repo (git reports the directory
  entry, which the exclude pathspec cannot match). The enumeration is now
  segment-filtered in JS, so vendored dependencies stay excluded while a
  nested project repository is still refused.

Suggestions and nits:
- The remote-URL guard reads url.*.insteadOf as well as remote.*.url and
  refuses any http(s) userinfo, including a token used as the username;
  ssh://git@ stays benign.
- Public keys (.pub) are no longer treated as credentials, and a
  backup.env~ fixture pins the trailing-tilde family.

Tests: 163 passed; six mutations verified red (filter overrides, vendored
segment filter, userinfo breadth, insteadOf enumeration, public-key
exclusion, backup.env~ pathspec).

* fix(verify-pr): close round-6 verification findings

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

* fix(verify-pr): close round-7 verification findings

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

* fix(verify-pr): address round-8 review findings

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

* fix(verify-pr): close round-9 review findings

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

* fix(verify-pr): close round-10 review findings

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-03 14:13:59 +08:00
Shaojin Wen 02b8b0108c feat(web-shell): show a Channel event timeline (#16106)
* feat(web-shell): show a Channel event timeline

Channels reported what they did only through stderr, the publication audit,
and the inbound task store, none of which the daemon could read. An operator
wondering why a GitHub comment got no reply had to read the daemon log.

Every Channel now appends one bounded journal per instance: the inbound
messages it accepted, the ones it skipped and the machine reason for each
skip, publication outcomes, inbound task transitions, and poll results. The
journal holds routing decisions and identifiers only, never message text; it
rotates at 1 MiB to a single backup, and a write failure is swallowed into one
stderr line so diagnostics cannot break message handling.

The daemon serves the journal through GET /workspace/channels/events and its
workspace-qualified form, with limit, type, channel, and since filters, a
32-file and 2 MiB read bound, and the existing trust and client checks. The
channel parameter repeats once per instance name because a Channel name may
legally contain a comma.

The Web Shell gains a Channel events page and sidebar entry that render the
cross-Channel timeline for the resolved workspace, filtered by Channel and
event type and refreshed every five seconds, gated by channel_management.

Docs: the design in English and Chinese, the serve protocol, and the Channel
user guide. Channel tests isolate QWEN_HOME so journals never land in a real
home directory.

* fix(web-shell): scope Channel events to the selected workspace

Review found four correctness gaps in the event timeline.

The page read the provider's daemon-wide primary cwd, so a Channel started in
a registered secondary workspace could never be inspected. It now offers the
same workspace selection as the management page and passes the selected cwd
to both hooks; changing the workspace resets the Channel filter.

The poll loop captured the adapter's detail before the poll ran, so the first
success had no counters and every later one carried the previous poll's
counters against the current timestamp. The field is now cleared before the
poll and consumed after it settles, on both the success and failure paths.

The 32-file read cap was applied before the Channel filter, which made an
explicitly requested, quieter Channel unreachable once busier peers had newer
files. Candidate files are now narrowed by the requested identities first.

Channel identities were truncated to 80 code points while the management API
accepts 255 UTF-8 bytes, so longer legal names never matched their own filter
and names sharing a prefix became indistinguishable. The identity now keeps
its full legal length; only the UI shortens it for display.

Each fix has a regression test that was mutation-checked against the previous
behavior. Design (both languages) and the Channel user guide now describe the
workspace selection, the poll-detail timing, the filter-before-cap ordering,
and the identity limit.

* fix(web-shell): mask Channel events cached for another query

The workspace selector changes the query without remounting the hook, and the
underlying resource keeps its previous data while a request is in flight or
after it fails. Selecting a second workspace therefore showed the first
workspace's rows under the new selector until the new request succeeded, and
kept them when it failed — including their Channel names in the filter list.

Each snapshot is now tagged with the query that produced it (workspace,
Channel, types, limit) and mismatched data is masked, the same way the Channel
management hook already does. Two hook-level regressions drive the real hook
and resource with a pending and a rejected second-workspace request; both fail
without the tag.

* fix(web-shell): keep Channel event reads off the daemon's critical path

A verification round measured the journal read at the documented cap: one
page poll took ~860 ms of synchronous I/O, an unrelated /health request waited
~790 ms behind it, and four concurrent reads serialized to 3.4 s. The route
runs on the daemon that also serves sessions, so a diagnostic page must not be
able to stall it.

The read is now asynchronous and chunked (64 KiB awaits) under tighter bounds:
256 KiB per journal file, 4 MiB per request, 32 files, freshest first. On the
same 32-file / 32 MiB fixture the read is ~175 ms, a bystander /health stays at
6 ms p50 against a 2 ms baseline, and four concurrent reads finish in ~630 ms.

Two reporting gaps go with it. Anything the bounds clip — dropped files or an
exhausted byte budget — now sets `truncated`, so the page cannot show a
timeline that silently omits whole Channels. And a scope directory that cannot
be read propagates instead of returning an empty list, so the route answers
5xx rather than claiming the workspace's Channels did nothing; a missing
directory still means "nothing written yet".

The page also stops polling while its tab is hidden and reloads when it
becomes visible, so a forgotten tab no longer reads journals every 5 s.

Tests pin the two behaviors the wire depended on but the suite did not:
reading events that survive only in the rotated file, and clamping a limit
above the maximum. Both new mutations are killed, as are the truncation,
unreadable-directory, and hidden-tab guards; a bystander-timer test pins that
the read yields to the event loop.

* fix(web-shell): report a clipped journal tail as truncated

The per-file read window introduced with the asynchronous read is 256 KiB,
but a journal grows to 1 MiB before it rotates. `readTail` skipped the head of
a taller file without telling anyone, so a single busy Channel lost its own
older events while the response still claimed `truncated: false` — the exact
completeness contract the previous commit set out to fix. A 1 MiB journal of
1020 records answered 255 events with `truncated: false` at `limit=500`.

`readTail` now reports whether it started mid-file and the reader folds that
into `truncated`. The request budget is also spent in whole tail windows
instead of a final partial one, so its flag keeps the precise meaning "whole
journals were left out".

Three tests close the gaps this exposed: a journal taller than the tail window
must report truncation even when the returned rows fit the limit; the budget
fixture now uses twenty full-window journals whose combined rows stay under
the record limit, so only the budget can explain the flag; and a journal file
that cannot be read must fail loudly, like the directory case already does.
All four previously surviving mutants — tail flag, budget flag, budget debit,
swallowed file-read errors — are now killed.

* fix(web-shell): bound Channel event read retention

The events read parsed and retained every matching record across all
journals before applying `limit`, so an all-Channels poll on a busy
workspace held far more than the rows it returns. Trim the accumulator to
the newest `limit` once it passes four times that. A dropped record is
older than every kept one and the sort is stable, so the answer does not
change: a 60-scenario differential run against the previous behaviour
produced byte-identical output. Trimming counts as hitting the record
limit, so it sets `truncated` too.

Closes the remaining non-blocking review items as well. The provider
workspace action declares the full query type it already forwards, so the
interface no longer suggests that path cannot filter by Channel. The new
UI strings use the established 频道 label rather than introducing a second
word for Channel beside the existing sidebar entry; the design docs keep
渠道, which is what every other zh-CN design uses. The docs now state the
page's own request size (200) apart from the route default (100), and the
`truncated` comments describe every reason it can be set instead of only
the record limit. The NUL query-key separator is documented as
collision-free: instance names reject control characters, journal names
are sanitized, and a path cannot contain NUL.

* fix(channels): read a journal that fits the budget and keep aligned records

Reserving a full 256 KiB window per journal left a small journal unread
when the remainder was under one window, even when it covered that
journal many times over — coverage lost in exchange for a flag that was
already truthful. Reserve what the journal actually needs instead, using
the size the enumeration already stats, so the budget flag still means
"whole journals were left out" and a 200-byte journal is no longer skipped
beside seventeen full ones.

A tail window that landed exactly on a line boundary also dropped one
complete record, because the first line was discarded whenever the window
started mid-file. Read one leading byte and drop that line only when the
byte is not a newline. Folding the byte into the first chunk keeps the read
to one awaited call per chunk, which is what lets the daemon serve other
requests between them — now pinned by a test that compares the event-loop
turns a tall read takes against a short one over the same six journals. A
synchronous chunk read fails it.

Also records why an unparseable instant cannot satisfy `since` while an
unfiltered read still returns it, and pins both halves.

* fix(channels): collapse repeated reason-filter skips into a counted row

A real end-to-end run — the actual GithubAdapter over HTTP against a local
GitHub-compatible API, a real agent turn, the page in a browser — exposed an
amplification the seeded-journal runs could not. The poll cursor does not
advance while an inbound task is recoverable, so a task that failed and is
still retryable makes the provider return the same notifications on every
poll, and filtered notifications are skipped before any dedupe. Each poll
therefore re-journaled the whole batch: with a 2 s poll and 300 filtered
notifications the journal rotated and pushed the accepted/posted rows out of
the page's 200-row window.

Journal at most 20 reason_filter rows per poll and collapse the remainder into
one row carrying detail.filtered, which the page renders as a count in both
languages. The budget is per poll, so a genuinely new batch is still described
in full. Measured against the real adapter: 15 polls served 4,500 notifications
and wrote 315 rows instead of 4,500, without rotating. Against a stub that
honours both `since` and GitHub's read-state semantics, the redelivery stops
once the batch is marked read and a new batch gets a fresh budget.

The design docs record the mechanism precisely, including that an in-flight
turn cannot cause it because the poll loop awaits pollOnce.

* fix(web-shell): keep one Channel event polling loop across visibility restores

Restoring tab visibility while a journal request was still pending started a
second run. Clearing the timer handle cannot cancel an in-flight run, so both
runs settled, both called schedule(), and only the last timer handle was kept —
the orphan kept polling forever. Ordinary tab switches on a slow response
therefore multiplied journal reads permanently: one overlapping hide/show cycle
left two requests per interval, three cycles left four.

Guard the cycle with an in-flight flag so a restore during a pending request
lets that request finish and reschedule instead of starting a second loop. Both
read paths are time-bounded (the SDK's 30 s fetch default and the action
timeout), so the guard cannot stall polling. The regression test holds every
request pending, overlaps three hide/show cycles, and asserts one request per
interval afterwards; removing the guard reproduces the reported four calls.
2026-11-03 13:42:18 +08:00
FengandShaojin Wen 71119c6fa0 fix(core): avoid unsupported forced Anthropic tool selection (#15558)
* fix(core): avoid unsupported forced Anthropic tool selection

* fix(core): tighten Anthropic schema fallback instructions

* test(core): cover Anthropic fallback placement and streaming

---------

Co-authored-by: Shaojin Wen <[email protected]>
2026-11-03 13:12:27 +08:00
qqqys cb6df9d901 feat(acp): run Goal turns on the shared loop (#16131)
* feat(acp): run Goal turns on the shared loop

With experimental.unifiedAgentLoop on, ACP's Goal turns now use the
shared round, the shared tool batch and the client's stop point. A
runtime continuation is a Goal round and a user prompt while a Goal runs
is a UserQuery, each under the turn's permit, turn key and origin. ACP
keeps the turn's scheduling and settles cancelled, cut-short, handed-off
and tool-ended turns; the client finishes a turn at its stop point and
pauses a Goal whose round fails, loops or reaches the Stop hook's cap,
with the reason ACP gives it.

Loop protection that stops a Goal turn now pauses the Goal, as on the
other surfaces, instead of letting the runtime schedule another
continuation.

Core: userTurnRecordedByHost also covers a Goal round whose message the
host recorded, and a stop policy's continuation keeps the send's Goal
fields so it runs under the same permit.

The ACP parity suite gains a Goal scenario run with chat recording on.

Part of #15327.

* fix(acp): keep a Goal turn that took input as its response finished

On the shared round, input whose drain answers after the response
finished is taken without cutting the response. The Todo Stop Guard then
gives way to it, and the client finished the Goal turn at its stop point.
ACP still answered the input with a Steer round under the finished
permit, which the client refused, so the turn failed and the input was
lost.

When a Goal turn's round took input and the guard lets it stop, ACP now
ends the round's signal without marking the response as cut, so the
client leaves the turn to ACP and the next round answers the input under
the same permit, with the prefix of input received while the model
responded. Once the Stop hook's cap has paused the Goal and released the
permit, a round that still answers input runs outside the Goal.

* fix(acp): run memory tasks for user Goal turns and mark continuations delivered at the stop

When the client finishes a user prompt's Goal turn, it returns before its
memory tasks, so ACP no longer treats such a round as having run them and
runs them itself when the prompt ends.

A runtime continuation is also marked delivered when the client reaches its
stop policies, so the mark no longer waits for a model event after the
request that may never come before the client finishes the turn.

Also share the "model started, continuation delivered" step between the
shared and current paths, and note why the shared path's loop-stop flag
stays as a fallback.
2026-11-03 12:51:36 +08:00
Shaojin Wen 95ef544479 test(managed-agent): reserve Hosted gate ports below the ephemeral range (#16174)
Hosted ITs reserved each port they hand to Spring, the Harness, a Spring
fixture or the Runtime Broker by binding port 0 and closing the socket.
The number stayed in the kernel's ephemeral range, so anything binding
port 0 or opening a connection could take it before its owner bound it;
9 of 240 recent CI runs lost a cell that way.

HostedGateTestSupport.reservePort() now hands out numbers from
[20000, 28000) with one counter per fork, probing each on 127.0.0.1,
and falls back to kernel-chosen numbers (never repeated in the fork)
on a host whose ephemeral range starts below 28000. Every Hosted
reserve-and-close site uses it, and HostedMonitorRunFaultGatesIT's
private copy of the same allocator is removed.

HostedTeamLeadMySqlIT's boot retry looked for the Harness's old move
message, which --profile hosted-harness no longer prints; it now treats
the exit message as a port conflict, and its teardown deletes the
attempt's rows even when settling the tenant's work fails.
2026-11-03 12:51:19 +08:00
jinye 07ed0e260e feat(managed-agent): support full model configuration at session creation (#16153)
* feat(managed-agent): support session model configuration

* codex: fix CI failure on PR #16153

* codex: address PR review feedback (#16153)

* codex: address PR review feedback (#16153)

* codex: address PR review feedback (#16153)

* codex: address PR review feedback (#16153)
2026-11-03 04:23:45 +00:00
9c54abcd3f test(core): pin hook error-text redaction with value-level assertions (#11785)
Refs #11760 item 1 only

Co-authored-by: 易良 <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-11-03 11:42:19 +08:00
0191185b10 test(permissions): cover persistent rule removal (#13818)
* test(permissions): cover persistent rule removal

* test(permissions): pin the removal contract for padded, duplicate and stashed rules

Padded argument: parseRule stores the trimmed raw while the lookup compares the caller's argument literally, so a padded removal is a no-op. Pinned deliberately so the asymmetry is a written-down choice.

Duplicate raw: seeding through config delivers the same raw twice (cli config.ts concatenates permissions.allow with the deprecated tools.allowed), so only the first copy is removed and the survivor keeps the grant.

AUTO mode: the wildcard lives in strippedAllowRules.persistent, which the lookup does not scan, so removal returns false and restoreDangerousRules re-attaches the grant.

Each case is mutation-verified: replacing the body with a boolean-preserving remove-all filter fails the duplicate case, normalizing the lookup key fails the padded case, and adding a stash purge fails the AUTO case.

---------

Co-authored-by: TYY-2005 <[email protected]>
Co-authored-by: 易良 <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-11-03 11:42:02 +08:00
Shaojin WenandQwen-Coder 1301ec30b1 refactor(managed-agent): narrow the four deferred store consumers to their role interfaces (#16121)
* refactor(managed-agent): narrow the four deferred store consumers to their roles

Complete the deferred migrations from the AgentStateStore role split
(#15962). ChildWorkspaceService, WorkspaceRuntimeResolver and
ManagedAutomationService now inject SessionQueryStore; TaskCancelCoordinator
injects SessionLifecycleStore. Each consumer's calls were verified to be a
subset of the target role. They were left on the composite only because the
automated detector misreads their this(...) constructor delegation as
forwarding. Type-only; the ledger note is updated in both languages.

Focused build green: 84 tests (incl. the real-Spring-context
ChildWorkspaceServiceTest), ModuleArchitectureTest, 0 Checkstyle violations,
all 7 modules compile.

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

* docs(managed-agent): broker wiring is no longer a multi-role composite consumer

Address the non-blocking review note: EmbeddedRuntimeBroker forwards the
store only into WorkspaceRuntimeResolver and ManagedRuntimeCoordinator,
which both take SessionQueryStore, so the ledger no longer lists broker
wiring among the multi-role dependencies that keep the composite, and names
EmbeddedRuntimeBroker as a candidate for the same narrowing. Both languages.

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

* docs(managed-agent): fold the four narrowed consumers into the audit tally

Address review R1-1: the consumer audit still read 18 single-role consumers
while a appended sentence recorded four more narrowed since, so the tally and
per-role groups understated the set by four. Fold ChildWorkspaceService,
WorkspaceRuntimeResolver and ManagedAutomationService into the query group
(4 -> 7) and TaskCancelCoordinator into the lifecycle group (8 -> 9), raise the
total to 22, and reword the appended sentence as a deferral-history note rather
than a separate migration list. Both languages; mechanical count verifies 22
with the four in their role groups, matching the code's injected types.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-03 11:41:33 +08:00
e4cf95e774 feat(config): add operator model catalog refresh switch (#16066)
* feat(config): add operator model catalog refresh switch

Honor a settings-level refresh switch from operator scopes, including bare mode, and skip the background models.dev request when usage statistics are disabled. Workspace settings cannot re-enable the refresh, and the existing environment-variable off switch still wins.

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

* style(config): make catalog refresh change set prettier-clean

`Lint & Static` failed in the incremental Prettier step (run 41303341242,
job 124838743484) on six files that this PR itself touches:

- docs/users/configuration/settings.md
- packages/core/src/config/config.ts
- packages/core/src/config/config.test.ts
- packages/cli/src/config/config.test.ts
- packages/cli/src/config/execution-sandbox-settings.test.ts
- packages/cli/src/config/settingsSchema.test.ts

Applied `prettier --write` with the repo config (prettier 3.6.1, the
version pinned in pnpm-lock.yaml). Formatting only: no behavior change.

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

* fix(cli): mirror modelCatalogRefreshEnabled in mcp reconnect

The throwaway Config built by qwen mcp reconnect passed usageStatisticsEnabled
but not modelCatalogRefreshEnabled, so the catalog-refresh gate defaulted to on
and the command still contacted https://models.dev/api.json even when the user
had model.catalogRefresh: false. The env var has no catalog equivalent, so only
the settings value is mirrored, matching loadCliConfig's resolution.

The reconnect suite passes at this head (38 passed).

* fix(config): keep the operator catalog refresh opt-out when a project replaces `model`

A trusted workspace that replaced the whole `model` section (`null`, `false`,
`[]`, or a string) left `stripSettingKeys` nothing to strip, and
`customDeepMerge` then assigned that value over the merged object. The
operator's `model.catalogRefresh: false` disappeared, `loadCliConfig` read
`undefined`, and core fell back to `true`, so a host that opted out of the
models.dev download fetched it anyway.

Restore the operator value in `mergeSettings` the same way
`tools.executionSandbox` already is, and state that the
`privacy.usageStatisticsEnabled` route is not workspace-restricted.

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

* fix(cli): carry the operator catalog refresh opt-out into the sandbox

The container mounts only the user settings dir and builds its env
passthrough from `process.env`, so a System-scope
`model.catalogRefresh: false` never reached the sandboxed CLI and it
still fetched models.dev. Set the refresh env var at the sandbox
handoff, which covers both the docker/podman passthrough and the
seatbelt child env.

Also move the operator scope reduce into a shared
`selectOperatorCatalogRefresh` beside `selectOperatorExecutionSandbox`
instead of keeping a second copy of the rule, and pin that a project
which replaced the whole `model` section keeps its own keys.

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

* fix(cli): carry the usage-statistics opt-out into the sandbox handoff

Core refreshes the models.dev catalog only when both
`modelCatalogRefreshEnabled` and `usageStatisticsEnabled` are true, but the
sandbox handoff exported the opt-out for `model.catalogRefresh` alone. A
System/SystemDefaults-scope `privacy.usageStatisticsEnabled: false` is
neither mounted into the container nor forwarded, so the containerized CLI
still fetched models.dev despite the documented second opt-out.

Resolve the privacy switch with the same helper `loadCliConfig` uses, so the
env precedence matches, and export the opt-out when either half is off.

Tests: cover the usage-statistics direction and the "no opt-out" negative
case, and pin the `model.catalogRefresh` workspace strip by key so deleting
the restricted-list entry fails instead of removing an iteration.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-11-03 11:26:02 +08:00
jinyeandQwen-Coder e42e57fce8 refactor(managed-agent): simplify row locking and runtime verification (#16139)
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-03 03:15:52 +00:00
Shaojin Wen f724e203ec fix(web-shell): bind config sync to the attachment, not the SSE stream (#16170)
* fix(web-shell): bind config sync to the attachment, not the SSE stream

The session config-ordering domain was recreated when the event stream was
opened, which happens after the load effect awaits its metadata. Because the
attachment is deliberately published before that await so the transcript paints
one round-trip earlier, a first-prompt config write could still be in flight when
the recreation landed. Recreating the domain invalidated the write's ownership
token, so a value the daemon had already confirmed was silently discarded, and
the reconciliation gated on the same stale token was skipped with it. A reasoning
effort chosen before the first prompt was therefore lost for good, and the
composer kept advertising the pre-choice value.

Establish the domain once, when the session becomes the current attachment, and
let stream setup read it instead of recreating it. The window closes without
moving when the attachment is published, so first-prompt latency and the early
transcript paint are unchanged, and every staleness guard is left intact.

Split the live-daemon settings inclusion/exclusion coverage into one test per
scenario sharing a single daemon, so each scenario owns a full timeout budget
and a failure names the scenario rather than reporting a bare timeout inside a
twelve-navigation loop.

One existing test asserted the current mode was undefined after an approval-mode
write had been confirmed; that held only because the same recreation discarded
the confirmed value. Its mock now reports the confirmed mode on later reads and
the assertion requires that value to survive, which is strictly stronger.

Fixes #16065

* test(web-shell): make the config-sync regression test discriminating

Review follow-up. The reasoning regression test could not tell "the
confirmed write was applied" from "a later context read republished it":
its mock answered every read from the already-updated state, so disabling
the write's apply while keeping the reconciliation left it green. Hold
every post-write read on a deferred and assert the confirmed value while
that read is still pending, so only the apply can satisfy it.

Share the live-daemon settings fixture with the existing spawnDaemon
harness rather than an inline spawn, poll and kill sequence. Boot failures
now carry the daemon's stdout and stderr instead of a bare Playwright
timeout, and the helper clears its boot timer and listeners on settle.
The helper appends an initialize-timeout flag, so this is not a pure
refactor; cliBin stays explicit so TEST_CLI_PATH cannot redirect it.

Record why the bind-site domain reset cannot be pinned in isolation:
read-sequence monotonicity and the outgoing stream's own teardown already
invalidate anything captured before a re-attach.
2026-11-03 11:14:40 +08:00
Shaojin Wen c26a32006a fix(serve): keep record_artifact guidance out of Managed Runtime worker results (#16148)
* fix(serve): keep record_artifact guidance out of Managed Runtime worker results

The worker builds its tools from a configuration that left artifact
recording at the core default. A Hosted Session declares no
record_artifact and the worker forwards only response parts, so every
Shell result for a command not proven read-only told the model to call a
tool it does not have, and write_file claimed artifact-like files were
recorded when nothing records them. The reminder's read-only check also loaded the Bash
AST parser in every sandbox for each successful Shell call.

Turn artifact recording off for the worker's tools regardless of env
flags, as the shared execution worker already does. The local Managed
engine's provider worker, which hands artifacts to its client, is
unchanged.

Fixes #16147

* fix(serve): keep artifact guidance out of the CSI files worker too

Add a Config option, artifactRecordingSupported, for hosts that expose
no record_artifact tool and forward no artifact metadata. Unlike
artifactEnabled, the artifact env flags cannot turn recording back on.
The Managed Runtime worker, the CSI files worker and the shared execution
worker now pass it instead of overriding isRecordArtifactEnabled on the
instance.

The CSI files worker's write_file and edit no longer tell the model that
a file was recorded as a workspace artifact. The tests now derive that
claim from formatRecordArtifactReminder() instead of a literal, cover
edit, and assert that a worker Shell call makes no call into the Bash
parser.
2026-11-03 10:26:15 +08:00
276a447ef0 fix(ci): interleave PR review endpoint cohorts and report final routes (#16151)
* feat(ci): support same-model endpoint splits in PR review canary

The endpoint override derived the cohort by comparing the routed model
name against the configured canary/default models, so configuring the
SAME model on both cohorts — a pure quota split across two gateways —
labelled every PR 'default' and silently disabled the override.

Derive the cohort from the bucket predicate itself instead
(review_cohort_for_bucket: same clamp, same PR-number bucket test as the
model router), so model name equality no longer matters. The notice's
cohort field remains the attribution when both cohorts run one model.

* fix(ci): interleave review endpoint buckets and report final routes

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

* style(ci): apply Prettier to the review endpoint bucket test

The Lint & Static lane runs `node scripts/lint.js --prettier` on the
files a PR touches, and this PR's own test file was not formatted:
three `expect(...).toBe(...)` lines exceed the print width, so the
incremental lint step exited 1 on every head.

Formatting only - no assertion or fixture changed.

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

* fix(ci): share the endpoint host derivation and pin the cohort notice

- Extract openai_base_host() so the proxy bypass and the cohort notice
  derive the host from one place; the notice cannot name a host the
  traffic was not routed through.
- Drop the comments that still describe the removed model router.
- Pin the notice's bucket count (8% -> bucket 87/100), make the
  noKey/badUrl rows exact strings, and assert initial_openai_host so the
  three mutations that survived this suite now fail.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-11-03 02:20:46 +00:00
Shaojin WenandQwen-Coder 47fcd5131d fix(acp-bridge): compact live tool-result displays for session replay (#16172)
* fix(acp-bridge): compact live tool-result displays for session replay

Opening a Web Shell session whose live turn edited large files could take
tens of seconds: the session/load response embeds the daemon replay
journal, and each edit result event carried the whole old and new file
contents twice (diff content plus rawOutput). One observed load response
reached 39.6 MB although the on-disk transcript was only 4.3 MB, because
the recording path truncates file-diff displays while the live ACP path
emitted them raw; adaptive journal growth then retained tens of MB per
session and every open re-downloaded and replayed all of it (#16171).

Compact the display where it becomes wire content and rawOutput, reusing
the recording-purpose compaction so warm replay shows exactly what cold
replay from the transcript already shows. Small diffs stay byte-for-byte
intact; oversized edits degrade to the saved-session preview.

Transcript records can carry leaner displays than the live producer emits
(blocked spawns have no task prompt), so the shared compaction utility is
hardened to pass non-string fields and malformed list entries through
instead of throwing; well-formed displays compact exactly as before.

* docs: record real-daemon A/B verification for live replay compaction

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-03 10:19:36 +08:00
易良andQwen-Coder 5b7f9ec1c3 fix(ci): repair desktop RPM dependency and teardown checks (#16154)
* fix(ci): repair desktop rpm dependency and teardown checks

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

* test(desktop): use a counted space class in the EPEL checksum pin

`scripts/lint.js --eslint` runs on every file the PR touches, and the new
rpm dependency test spelled the two spaces in the pinned `echo` line as
literals: `no-regex-spaces` rejects them, so Lint & Static failed on the
incremental ESLint step.

`[0-9a-f]{64} {2}` matches the same two spaces the workflow emits; the
assertion still pins exactly what the release step writes.

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

* fix(desktop): keep the runtime group as a teardown target

Teardown collected daemon pids through the zombie-aware liveness predicate and
escalated SIGKILL only on leaders that were still alive, so a leader that had
already exited — reaped, replaced, or an unreaped zombie — silently dropped its
process group, which is teardown's only handle on it. The children the leader
spawned (MCP servers, tool subprocesses) then survived and kept writing under
the workspace about to be deleted, and the smoke reported a pass.

Collect every recorded pid, escalate on every collected group after the drain,
and keep the zombie-aware predicate on the "survived teardown" report so the
false positive this PR exists to fix stays fixed. The new fixture drives a
runtime whose leader exits while a writer in its group ignores SIGTERM.

Also pins the EPEL download URL and digest literals, drops the duplicate local
liveness predicate in favour of the shared helper, and covers the signal-probe
error arm (ESRCH gone; EPERM/EACCES alive, the rule the core helper documents).

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

* fix(desktop): decide teardown on the runtime group, not its leader

The round-2 escalation signals the recorded pid's group (`kill(-pid)`), but
the verdict and the drain still asked `isProcessAlive(pid)`, which answers
"gone" as soon as the leader exits — the exact topology
`SMOKE_FIXTURE_EXIT_AFTER_SPAWN` reproduces. In that state `waitForExit`
returned on its first poll, so the 5 s drain was skipped and the
`survived teardown` branch was unreachable.

`isTeardownTargetAlive` probes the group first and falls back to the leader
where there is no group to signal (win32), so the drain and the leak verdict
cover the children the group can still hold.

Also drops the "mirrors the canonical … keep both in sync" claim from the
desktop probe's comment: it diverges from core's `isPidAlive` on the
pid <= 0 guard, the zombie exclusion on the EPERM/EACCES arm, and the
vanished-/proc answer. The comment now states those differences and why
teardown wants the dead-on-ENOENT direction.

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

* fix(desktop): stop counting unreaped zombies as a live teardown group

`waitForExit` asks `isTeardownTargetAlive` whether a daemon's group still
exists. `kill(-pid, 0)` also answers for members that can no longer write, so
a daemon that exited and that the container's PID 1 has not reaped kept its
group "alive": the drain burned its five-second window, the second probe still
returned the pid, and teardown reported "the packaged runtime survived
teardown" for a runtime that was already gone.

On Linux the group probe now counts only members that can still write: scan
`/proc` for entries whose pgrp is the recorded pid, and report the group
stopped when every member is a zombie. A scan that observes no member keeps
the group probe's answer, so a hidden /proc cannot end a drain early, and the
group handle still wins over the leader handle.

Measured on a real zombie group (setsid + _exit, parent never reaps): /proc
state Z, `kill(-pgid, 0)` exit 0, `isProcessAlive` false, and
`isTeardownTargetAlive` true before this change, false after it.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-03 10:18:16 +08:00
jinyeandQwen-Coder 90eb13e4c6 refactor(managed-agent): Make runtime backends optional Maven modules (#16141)
* refactor(managed-agent): Split runtime backends into optional modules

Extract five backend implementations behind the shared provider SPI and support selected-backend executable distributions. Preserve SQL ownership, defer unavailable historical work, and make Spring own SSH connection cleanup.

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

* refactor(managed-agent): Narrow backend split to Maven modules

Keep optional backend dependencies and safe assembly while deferring selective standalone distributions. Restore default packaging and document the provider configuration.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-03 02:04:43 +00:00
Shaojin Wen a6f1885833 test(autofix-pr): re-run a timed-out job and lose its answer in the chain test (#16138)
The fake gh only re-ran FAILURE jobs and applied loseNextAnswer to comment
POSTs only, so the helpers' handling of a timed-out job's re-run and of a
re-run whose answer is lost could not be reached from the chain test.

- `run rerun` re-runs FAILURE and TIMED_OUT jobs, refuses a run with no
  failed job, and with loseNextAnswer queues the jobs then fails the way gh
  words a non-403 error.
- A job conclusion the fake does not list is refused as unsupported, on
  every path that reads a run.
- FAKE_GH_WORLD, and the names derived from it, are read after the call is
  logged and inside the guard, so a missing or malformed world shows as
  unsupported instead of a bare crash.
- New case: a timed-out job whose re-run's answer is lost is left for a
  person; when the re-run times out again, rerun refuses a second one, and
  status lists both. The re-run's attempt key is pinned, here and in the
  re-run case.
- Design §12 (both languages) lists the conflict case and the new case.
2026-11-03 09:17:45 +08:00
Bob.qwencodeandgithub-actions[bot] 212fedbdc7 chore(release): sdk-typescript v0.1.27 (#16164)
* chore(release): v0.26.6

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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-03 07:01:30 +08:00
Bob.qwencodeandgithub-actions[bot] 5e267805c3 chore(release): v0.26.6 (#16163)
* chore(release): v0.26.6

* docs(changelog): sync for v0.26.6

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-03 07:00:07 +08:00
jinyeandQwen-Coder 0441faff21 refactor(managed-agent): trim store delegation boilerplate (#16142)
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-03 00:12:05 +08:00
易良andQwen-Coder 4dce337ea2 feat(ci): let the PR review canary cohort carry its own endpoint (#16130)
* feat(ci): let the PR review canary cohort carry its own endpoint

The review canary split (QWEN_PR_REVIEW_MODEL_CANARY/PERCENT) only moves
the model name; both cohorts share REVIEW_OPENAI_BASE_URL and its key, so
a rate-limited canary model also burns the shared gateway's quota — the
shape behind the 429-storm review timeouts.

Add an optional endpoint pair for the canary cohort:
QWEN_PR_REVIEW_BASE_URL_CANARY (variable) + REVIEW_OPENAI_API_KEY_CANARY
(secret). When the bucket routes to the canary and both are set, base URL
and key move with the model and the canary host joins NO_PROXY. A URL
without its own key, or an unparseable URL, degrades to the default
cohort with a warning — forwarding the default key to another provider
would leak it. A canary model identical to the default model disables the
override (buckets are indistinguishable). The cohort notice now prints
the resolved cohort so rollouts stay attributable.

Unset, behavior is byte-for-byte today's: model-only split on the shared
endpoint.

* fix(ci): drop inert canary NO_PROXY append and record the workflow's new size

The canary endpoint override appended its host to NO_PROXY, but nothing in this
job consumes it: configure_qwen_network unsets HTTPS_PROXY/https_proxy/
HTTP_PROXY/http_proxy before the override runs, qwen does not consult NO_PROXY
when building its proxy agent, and the gh/git wrappers that do get the proxy
back only talk to github.com. Removing the two exports also removes the comment
that claimed a proxy-bypass symmetry the step cannot produce.

Lint & Static was red in "Check workflow file size": 276955 bytes against a
recorded 272192 with a 4096 allowance. The growth is the feature itself (the
env pair, the override block, the resolved-cohort field), not prose that could
move into a sibling file, so the baseline moves with the file rather than the
rationale being cut to fit.

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

* refactor(ci): simplify canary endpoint validation

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

---------

Co-authored-by: qwen-code patrol <[email protected]>
2026-11-02 23:22:43 +08:00
Shaojin Wen 14de8a035e feat(verify-pr): inherit --publish consent on same-session re-verification (#16112)
* feat(verify-pr): inherit --publish consent on same-session re-verification

A maintainer who ran `/verify-pr <n> --publish` and then typed the
"有更新了,重新验证" follow-up trigger had to confirm publishing again:
the skill inherited the previous round's coordinates but deliberately
excluded publish consent, so every re-verification of an already
published PR repeated a confirmation the maintainer had just given.

Let a follow-up round inherit publish consent from the most recent
round for the same PR in the same session. The round must announce the
inheritance in one line before posting, `--no-publish` on the trigger
opts out and breaks the chain, and the most-recent-round semantics keep
a quiet round from reviving an older consent. Cross-session rounds
still require an explicit request: the phrase alone posts nothing, and
a published comment found on the PR is evidence of a past round, not
consent for a new one. The CI lane and watch mode are unchanged.

Update the two pin tests that fixed the old per-invocation-only rule
and pin the inheritance, announcement, opt-out, and cross-session
boundaries.

* fix(verify-pr): carry inherited publish consent through follow-up chains

Review of the first round found the consent chain broke after a single
inheritance: R1 invoked with --publish, R2 a bare trigger that inherited
and published, R3 a bare trigger whose most recent round (R2) was not
itself invoked with --publish — so R3 fell back to asking again, the
exact friction this change exists to remove.

Model consent as a per-PR state of the session instead of keying it to
the latest invocation: a --publish invocation grants it, a fresh
/verify-pr round without --publish or a trigger carrying --no-publish
withdraws it, and a bare trigger keeps it unchanged, so consecutive
bare triggers publish under the original grant until a round withdraws
it. Watch rounds keep their own standing consent and never change this
state. Also close the two review suggestions: the Hard rules bullet now
recognizes a request inherited within the same session, and the
argument hint lists --no-publish so the opt-out is discoverable.

Pins cover the grant/withdraw/keep mechanics, the three-round chain,
the watch boundary, the Hard rules clause, and the updated hint.

* fix(verify-pr): grant session consent on any explicit publish request

Second review round found two gaps. The design docs for the assets
coordinates still stated that publishing consent is per invocation,
which reads as an invariant and could silently revert the feature;
both language versions now defer to the skill's session-consent rule
while keeping "coordinates are not consent". And the grant keyed on
the literal --publish flag, while the publish rule also accepts an
explicit request in words — a words-request round published but seeded
no consent, splitting the friction this PR removes by request form.
The grant now rides on the explicit request, flag or words alike, and
the withdrawal matches it.

Also close the deep verification's coverage survivors: the withdrawal
is pinned as one contiguous sentence (a fresh round keeping consent
now fails), the chain's upper bound, the announcement's naming duty,
and the evidence-of-a-past-round clause each have pins, and an
inherited publish now carries its one-line announcement into the
comment body immediately before the attribution footer, so a PR
reader can tell it from a per-round request.
2026-11-02 22:22:00 +08:00
易良andQwen-Coder b142071ab2 fix(core): report redirects on cd segments (#16129)
* fix(core): report redirects on cd segments

Preserve redirect-derived operations when resolving cd, pushd, and popd segments so Write rules see targets opened before the cwd changes.

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

* test(core): rename stale cd redirect test title

The title said 'ignores redirects attached to cd' but that is the
behavior this PR removes; the row survives only because >/dev/null is
filtered. Rename it to describe what it actually pins now.

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-02 22:03:14 +08:00
易良 c2921217ef ci: gate on the OS temp directory and shorten /tmp retention to 1d (#16126)
* ci: gate on the OS temp directory and shorten /tmp retention to 1d

The 2026-10-31 hz incident: /tmp tmpfs hit 100% inodes while the
workspace and RUNNER_TEMP sat on a data disk with millions free, so
every disk-floor gate was green and jobs died on mkdtemp ENOSPC
mid-suite. The gate now always checks the OS temp directory
(deduplicated against caller arguments); the tmpfiles policy drops
7d->1d because the measured 120-260k files/day steady state exceeds
the tmpfs inode ceiling at 7d. Mutation-verified: dropping the
os.tmpdir() check reds the suite.

* test(ci): cover the TMPDIR-unset default and pin the 1d retention

Review findings: the new OS-temp test never executed the production
incident path (TMPDIR unset -> /tmp default) — a stripped default
survived the suite green; and the 7d->1d retention change had no test
coverage — a revert shipped the incident again. Add the unset-TMPDIR
sub-run, a set-equality dedup assertion, and a qwen-docker-cleanup
suite pin on the exact tmpfiles line. Mutations verified red.

* test(ci): pin RUNNER_TEMP apart so the /tmp fallback test bites

The first version of the TMPDIR-unset test let the RUNNER_TEMP default
(/tmp) satisfy the assertion, so a stripped ${TMPDIR:-/tmp} default
survived green — the exact regression the test exists to catch. Pin
RUNNER_TEMP to a distinct directory; the /tmp sample can then only come
from the OS-temp append. Mutation now reds.

* test(ci): unset TMPDIR as empty string; destructure was a no-op

The previous fix still failed on clean trees: spreading a destructured
env cannot DELETE a key — process.env.TMPDIR re-entered via runGate's
own spread, so the /tmp fallback never fired and the test failed
against the correct script. An empty-string TMPDIR is bash-unset for
${TMPDIR:-/tmp} (and bare for the regression), which is the exact
distinction under test. Clean 8/8; stripped-default mutation reds.

* ci(ecs-runner): enforce the /tmp retention from the owned daily service

Review finding: the 1d policy's enforcement agent was the distro's
systemd-tmpfiles-clean.timer, which can be masked on hardened images and
reports 'static' (unverifiable via is-enabled) on stock Ubuntu — with
nothing in the qwen stack running systemd-tmpfiles --clean, the measured
120-260k files/day steady state regrows past the tmpfs inode ceiling and
the 2026-10-31 incident class recurs with every test green. Add
ExecStartPost=systemd-tmpfiles --clean --prefix=/tmp to the daily
qwen-docker-cleanup.service, pin it in its test suite, and document the
enforcement owner in the README verification block. Mutation-verified.
2026-11-02 22:02:36 +08:00
a983503f02 fix(core): attribute shell notifications to the registering turn (#16125)
A background shell that settles mid-turn never reached the model until the
turn ended: ShellNotificationMeta carried no sourceTurnId, so
Session.#takeCurrentTurnBackgroundParts — which skips queued items whose
sourceTurnId differs from the current turn — could never match the item,
and delivery fell through to the end-of-turn drain.

Capture the registering turn in BackgroundShellRegistry.register(),
mirroring BackgroundTaskRegistry (getInvocationContext()?.promptId ??
promptIdContext.getStore()), declare it on ShellTask, and forward it
through ShellNotificationMeta into the Session shell callback. Both the
direct is_background: true spawn and the foreground→background promote
funnel through register(), so one assignment covers both; a promoted
shell is attributed to the promoting turn, which is the turn that ends up
waiting for the result.

Monitor and workflow callbacks have the same gap; left as a follow-up.

Guarded by two registry tests: the completion meta carries the
registering turn, and a shell registered with no turn context stays
unattributed — and still emits — rather than inventing an attribution.


Patrol-Run: qwen-issue-patrol/jmvz3emo2tv

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-02 22:02:00 +08:00
jinyeandQwen-Coder f643e579c5 fix(managed-agent): Restore profile-owned runtime verification (#16134)
Keep the shared Failsafe configuration in pluginManagement so ordinary
release verification does not activate environment-specific integration tests.
Preserve the dedicated profiles and executable artifact assertions.

Backport the same POM repair to the pinned 0.26.5 source with checked patch
digests, and verify both patches before applying either one.

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-02 13:50:11 +00:00
易良andQwen-Coder e7b86ae32d fix(mcp): warn about disabled tool typos on every discovery path (#16120)
Refs #16078

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-02 21:41:17 +08:00
Shaojin Wen 9336986835 feat(web-shell): make GitHub channel configuration usable (#16096)
* feat(web-shell): make GitHub channel configuration usable

The channel editor rendered every platform through one generic form, which
left the GitHub channel hard to configure and persisted settings its adapter
never reads.

- Render string-list fields that declare options as a checkbox list, so the
  fifteen notification reasons are pickable and explained instead of typed
  from memory. Unknown values written by hand stay visible and removable
  rather than disappearing on the next save.
- Lock the token input while local gh authentication is selected, clear a
  stored token when that switch is turned on, and drop a token that an
  existing useLocalGh selection has already overridden.
- Hide and stop persisting the direct-message policy for repository-scoped
  channels: the registry injects privatePolicy into every manageable channel,
  but the GitHub adapter only produces group envelopes, so the value never
  gates anything and only misleads.
- Name allowlisted groups repositories, validate owner/repo, and show the
  GitHub Enterprise API base URL shape.
- Split the catch-all credentials section into credentials, connection, and
  notification routing.

* test(web-shell): pin the GitHub editor capture to the reason checklist

The screenshot scenario now fails if the filter falls back to a free-text
input, so the published picture cannot drift away from the behaviour the
pull request claims.

* test(web-shell): pin the section split on a non-GitHub channel

The connection and routing sections key on field names, so they regroup gitlab and wecom fields too. Assert that split on a gitlab descriptor instead of leaving it implied by the GitHub assertions.

* fix(web-shell): match GitHub channel values the way the adapter reads them

Review round 1 on the editor found three places where the new controls
disagreed with the runtime or left dead configuration behind.

- The reason checklist compared values case-sensitively while the adapter
  lowercases both sides, so a stored COMMENT rendered as a stray sixteenth
  box beside an unchecked canonical one; ticking only mention then left
  comment notifications flowing. Compare case-insensitively and keep unknown
  entries in their original spelling.
- The pairing guard had no test witness: deleting it left the whole suite
  green while a legacy privatePolicy reopened the direct-message panel for a
  repository channel. Pin both directions — legacy private pairing must not
  open the panel, group pairing still must.
- Only privatePolicy was cleaned up; the equally unread allowedUsers survived
  every save and was still presented as an authoritative allowlist in the
  pairing panel. Drop the whole private-access set and stop showing it there.

* fix(channels): match GitHub repository groups regardless of case

The editor started validating repository allowlists as owner/repo, but a
repository path is case-insensitive while the runtime matched the group key
verbatim. An operator who typed qwenlm/qwen-code passed validation, saved, and
got a channel that connected and then dropped every notification as
not_allowlisted — the exact failure the validation was added to prevent.

Normalize on both sides so neither can drift:

- The editor writes the canonical lower-case key and shows stored allowlists
  in that form, so re-saving an older mixed-case entry migrates it. The
  per-repository settings are looked up under the normalized key, so retyping
  the case no longer discards that repository's sender policy.
- GroupGate and ChannelBase accept a platform group-ID normalizer, used for
  the allowlist, the sender policy and the per-group overrides alike.
  Normalizing only admission would have let a case-mismatched repository fall
  back to the open sender default, which is worse than dropping it.
- The GitHub adapter supplies the lower-casing normalizer; every other
  platform keeps matching group IDs verbatim.

Pairing approvals are unaffected: they are keyed by the chat ID the platform
reports, so they were already self-consistent.

* fix(channels): resolve the group history limit through the same lookup

Self-audit of the repository-casing fix found a third direct read of
config.groups: the per-group history limit still matched the key verbatim, so
a platform with case-insensitive IDs silently lost that override while the
allowlist and sender policy honoured it. Route it through groupConfigFor and
pin it with a case-insensitive channel test.

* fix(web-shell): keep the gh switch reversible and refuse ambiguous repositories

Review round 2 plus the independent verification round found three defects.

- Turning local gh authentication on queues the stored token for removal,
  because a token outranks gh. Turning it back off left that intent in place,
  so Save demanded a credential the operator never chose to delete and the
  switch could not be undone without retyping a PAT. Flipping it back now
  restores the stored token.
- Collapsing repository aliases could widen access. A stored allowlist may
  hold Owner/repo and owner/repo with different settings; the runtime resolves
  an exact key first, so keeping only one of them on an unrelated save dropped
  the effective sender restriction. The editor now refuses to save until the
  operator resolves the duplicate, and shows the allowlist without repeating
  the same repository twice.
- The reason descriptions described event types instead of the subscription
  reasons GitHub actually reports: comment means the account commented on the
  thread, state_change that the account changed its state, ci_activity that a
  workflow it triggered completed, and manual that it subscribed by hand.
  Rewritten in both languages against the REST notification contract.

The visual scenario also captured the dialog without scrolling, so the
checklist sat below the fold and the published picture did not show the
feature. It now scrolls the filter into view, asserts it is fully on screen,
and captures it separately.
2026-11-02 13:11:33 +00:00
qqqys 73474ef094 fix(mods): skip hooks on prompt.paint and ui.place instead of failing (#16124)
A Mod that hooked either event failed to initialize, which stopped a headless run before any model request. Both are now skipped with the startup line, like other events Qwen Code does not run yet. $.session.version() reports 2.1.316.
2026-11-02 12:50:30 +00:00
38b71299c8 feat(desktop): build and publish rpm installers (#16060)
* feat(desktop): build and publish rpm installers

Add Fedora 44 and RHEL 10 release roots, strict RPM smoke checks, and artifact naming that keeps each root distinct.

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

* fix(desktop): declare rpm runtime requirements and clear the yaml gate

The rpm legs built artifacts with no `Requires:` entries: Tauri's rpm bundler
writes them only from `bundle.linux.rpm.depends`, which nothing set, so
`verifyRpmDependencies` had no webkit soname and no glibc floor to find and the
release could not pass its own gate.

- Add `rpm-fedora44.conf.json` / `rpm-rhel10.conf.json` overlays declaring the
  webkit soname and each root's glibc floor (2.43 vs 2.39 cannot share one
  config), and pass the per-leg overlay to `tauri build --config`.
- Quote-free `container: ${{ matrix.container || null }}` stays as is with an
  inline yamllint disable: quoting the expression would turn a null container
  into an empty string. The shape test pinning that literal moves with it.

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

* ci(desktop): record the rpm legs in the workflow size baseline

`desktop-release.yml` is now 49858 bytes, past its recorded 45089 plus the
4096 allowance, so `Lint & Static` fails on the ratchet before any linter runs.
The growth is the two rpm legs: their matrix entries, the per-leg overlay
wiring, and the roots' comments. Moving that prose out would not pay for it —
the legs themselves are the bytes — so the number moves with the file in the
same PR, as the gate asks.

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

* style(desktop): apply prettier to the rpm legs and realign the workflow shape pin

Prettier flagged .github/workflows/desktop-release.yml and
packages/desktop/scripts/smoke-linux-installers.js on head 10acaadeba.
test-release.js pinned that container line with two spaces before the inline
yamllint disable comment, which prettier rewrites to one; .yamllint.yml has
min-spaces-from-content: 1, so the pin now follows the formatter instead of
blocking it.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-11-02 20:50:05 +08:00
jinyeandQwen-Coder d9d6fc4a4b refactor(managed-agent): split Agent and Session stores (#16090)
* test(managed-agent): audit lock order across store collaborators

Record the bilingual Store decomposition contract and entry ownership,
transaction, lock and caller inventories. Follow final collaborator fields
across source files while preserving owner-bound facade populations and
existing ambiguity, transaction and compiled-member checks.

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

* refactor(managed-agent): split Agent and Session stores

Move Agent and Journal responsibilities into immutable internal collaborators while retaining transaction facades and compatibility bridges. Resolve Journal dependencies at construction and migrate factories, fixtures, lock auditing and the bilingual implementation contract.

Record focused verification and outstanding full-gate requirements separately.

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

* fix(managed-agent): repair store decomposition verification

Keep declared field receivers visible across method-local type scopes while retaining fail-closed nested class handling. Preserve the Hosted CSI dependency identity assertion at its new owner.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-02 12:46:43 +00:00
易良andQwen-Coder 8aea64d5f7 fix(desktop): verify AppImage library closure (#16062)
Read every ELF's DT_NEEDED entries from the extracted AppImage and fail release when a dependency is neither bundled nor part of AppImage's host-provided baseline. Document the contract and cover the failing and repaired cases.

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-02 19:43:23 +08:00
BaboBen 84c4c70789 fix(channels): allow request owners to answer one-time tool permissions (#16087)
* fix(channels): allow request owners to answer one-time tool permissions

* docs(channels): record permission-owner verification
2026-11-02 19:41:43 +08:00
易良andQwen-Coder 5df87f7080 fix(desktop): stop the bundled runtime from the Windows installer (#16071)
An update started by a desktop build released before the app-side runtime stop can still leave the bundled Node daemon holding node.exe while NSIS replaces the runtime. Clear that lock from the installer pre-install hook by image path, wait boundedly for the image to become writable, and abort before copying unless PowerShell returns an explicit success. Pin the behavior in the desktop release gate.

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-02 19:40:13 +08:00
Shaojin WenandQwen-Coder 60f5c1ed95 docs(managed-agent): record the completed store constructor injection in the ledger (#16118)
#16061 replaced the seven ManagedAgentStore public setters with a single
@Autowired constructor holding final references, but the parent
structure-review ledger still described the setters as current state and
framed the conversion as future work. Update the D1 row note, the prose and
the P1 store narrative to the implemented state in both languages, keep the
setter investigation as pre-conversion context, and leave the physical
aggregate split as the remaining work.

Figures re-measured against origin/main (0 public setters, 1 @Autowired
constructor, final fields, 7657 lines); prettier and bilingual heading/link
checks pass. Documentation-only.

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-02 17:38:19 +08:00
qqqysandShaojin Wen 9c96f5aa82 feat(mods): run prompt.submit hooks in headless runs (#15905)
* feat(mods): run prompt.submit hooks in headless runs

Mods' prompt.submit hooks can rewrite, add context to, or drop each headless prompt before UserPromptSubmit, @ processing and the transcript see it. Terminals keep skipping these hooks until they raise the event. Includes the design document for the four steps.

* test(mods): update bundled worker load response expectation

* fix(mods): preserve downstream prompt submit outcomes

* fix(mods): isolate prompt fallback from host continuations

* fix(mods): preserve latest local prompt outcome in recovery

* fix(mods): hear only a message's text and take any answer after next

A stream-json message with an image block reached the prompt.submit
hooks as its serialized blocks, so they were skipped as too long or saw
JSON noise, and a rewrite carried that JSON into humanInput and
turn.start. The hooks now hear its text blocks; a rewrite replaces them
and the other blocks follow it.

A hook's answer after its next changes nothing, but it was still
validated, so a hook that only watches and returns nothing, or spreads
the event back, failed and logged a skip on every prompt. Only a drop
there is now checked.

* fix(mods): keep prompt.submit to typed text and drop empty rewrites

- Show only the rewritten text in the transcript and UserPromptSubmit,
  not the serialized non-text blocks of a stream-json message.
- Skip prompt.submit for SDK messages that answer a tool call or carry
  tool blocks, and for messages without text.
- Drop a prompt the hooks rewrote to nothing instead of sending the
  model an empty prompt.

* fix(mods): send each prompt context once on its own

The Mods' prompt.submit context and UserPromptSubmit's additionalContext
share one hidden block, and the block was only skipped when the
conversation already held it whole. A Mod whose context changes every
turn made every block new, so an unchanged UserPromptSubmit context went
to the model again each turn.

The injected part now records the contexts it joins, and a context counts
as delivered when an earlier block holds it, so only the contexts the
conversation lacks are sent. A block without that record, as after
resume, still counts as one context.

* fix(mods): keep a message's blocks in the transcript after a rewrite

A stream-json message with blocks other than text is recorded with those
blocks serialized after its text. A prompt.submit rewrite recorded the
rewritten text alone, so the same kind of message showed two shapes in the
transcript. The rewritten prompt is now recorded with the blocks after it.

Adds the headless E2E test plan and its results.

---------

Co-authored-by: Shaojin Wen <[email protected]>
2026-11-02 09:14:14 +00:00
qqqys 1e250b391b feat(acp): run re-entered, channel and Goal-proposal turns on the shared loop (#15902)
* feat(acp): run re-entered, channel and Goal-proposal turns on the shared loop

With experimental.unifiedAgentLoop on, channel prompts and Goal-proposal
approval turns run as a UserQuery on the shared round, the shared tool
batch and the client's stop point, and a retry, a continue and the
restore of an unanswered ask_user_question run there with a Retry first
round. ACP still strips and restores the re-entered attempt, still runs
UserPromptSubmit on a retry through admitUserPrompt, and still answers a
restored question with its own tool step.

Core: userTurnRecordedByHost also covers a Retry's todo work chain,
promptAdmission is accepted on a Retry, and the new input
historyPromptId gives a Retry's request the identity of the attempt it
replaces.

The ACP parity suite gains a channel prompt and a retry after a model
error.

* fix(acp): treat a re-entered turn's pushed first request as sent on every exit

On the shared round, whether the first request reached history is now
read when the round ends, on every exit. A retry whose request was
pushed and whose response then failed no longer puts the stripped
attempt back beside it, and a restore's one-shot notices are taken once
its answer is in history.

A continue whose first request never goes out, cancelled at the loop
top or failed, puts the interrupted run back as it was and keeps the
worktree notice its dropped message took pending. A restore cancelled
before its request went out keeps the full answer and takes its
notices.

The stripped-run restore and the Goal-proposal settlement block are
shared helpers. The parity suite runs the channel scenario from a
trusted parent so the channel mark survives, and adds a retry that
fails again. Docs and comments are corrected.

* fix(acp): keep a Goal proposal unsettled when its turn gives way or ends early

In plan mode, the Todo Stop Guard on the shared round now still yields
to a queued prompt or to input the round took, so a Goal proposal is not
started from a turn that gave way.

The client no longer drops a host-owned Goal proposal when a send ends
abnormally. The host takes it when its turn ends, so a cancelled
proposal turn still reports that the Goal did not start.

A Retry whose admitted prompt is blocked puts back the attempt the
client stripped from history.
2026-11-02 17:14:05 +08:00
jinyeandQwen-Coder 85943db2b3 fix(ci): repair Javadoc for the 0.26.5 managed runtime release (#16114)
Apply the already merged documentation-only repair to the original release source before the unchanged full verification and missing-only publication gates.

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-02 17:12:10 +08:00
qqqys 9d2ee877c2 feat(web-shell): plot per-request speed and failures in token usage (#16111)
* feat(web-shell): show request success rate and distinct model colors in token usage

The token usage panel tracked failed API calls but never showed them, and
models were hard to tell apart: the first two series colors were both blue,
every share bar started from the same blue, and the card had no color mark.

- Add a success rate to the overview and to each model card, with a status
  icon (ok / warning below 100% / error below 90%) and the failed count.
  Each retry counts as its own call, as in `api.totalErrors`.
- Add session output speed to the overview, weighted over all timed models.
- Replace the model palette with six categorical colors checked for
  color-vision-deficiency separation, with dark-theme steps. Colors follow
  the order models were first used, so a model keeps its color when the
  token ranking changes; models past the sixth share a neutral color.
- Add a stacked per-model share bar with a legend, and a color strip on
  each model card. Share text keeps the foreground color.

* feat(web-shell): plot per-request speed and failures in token usage

Token usage showed one average output speed per model, so a slowdown
mid-session or a few slow outliers looked the same as a steady rate,
and failed calls had no time or kind.

- Core keeps, per model in the stats buckets, the latest 200 timed
  responses (time, first token, generation window, output tokens,
  subagent) and failed calls by status code or error type with the
  latest 50. Session replay rebuilds both from stored events.
- The daemon stats route returns them as optional `timingSamples` and
  `failures`; older daemons omit them.
- Web Shell adds a speed-over-time chart: one dot per request in the
  model's color, a five-point median line, a toggle to first-token
  latency, failure marks, a nearest-request tooltip on hover or tap,
  and a legend that hides series.
- Model cards add an output-speed spread (p50, p10-p90 on a shared
  scale) and failures by kind.
- Add the design doc in English and Chinese, and point the token usage
  hierarchy doc at it for the model palette.
2026-11-02 08:26:53 +00:00
易良andQwen-Coder 292c0da525 test(install): pin Windows PATH persistence contract (#16072)
* test(install): pin Windows PATH persistence contract

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

* test(install): format the pinned PATH assertion

Prettier collapses the single-string expect(...).toContain(...) call at 76
columns, which the incremental lint gate rejected on the previous head.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-02 15:44:05 +08:00