Commit Graph
12022 Commits
Author SHA1 Message Date
jinyeandQwen-Coder 32d22f950c feat(managed-agent): support durable structured user questions (#16433)
Connect Hosted question actions, structured answers, durable continuation, and the WebShell question form. Cover canonical response replay and pending-question recovery across Harness and Java context restarts.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

* fix(serve): avoid redundant replay seed serialization

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

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-06 16:13:17 +08:00
2666e3552d fix(core): make persisted session pagination safe for equal mtimes (#16024)
* fix(core): make persisted session pagination safe for equal mtimes

listSessions() ordered files by mtime alone and resumed the next page with a
strict `mtime < cursor`. An mtime is not unique — bulk copies, backup restores
and coarse-timestamp filesystems all produce ties — so a group of files sharing
an mtime that straddled a page boundary was dropped while hasMore still reported
progress, then the next page came back empty: 25 files at one mtime with size 5
showed 5 items in total.

Sort on (mtime desc, name asc) so the order is total, and let the cursor carry
that position: nextCursor is now `"<mtime>|<name>"`. A bare number is still
accepted with its existing strict `<` meaning, so callers holding a legacy
cursor keep working, and the daemon's opaque string cursor now passes the
composite form through instead of rejecting it as invalid.

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

* fix(cli): carry the composite session cursor type through callers

listSessions() now hands back a "<mtime>|<name>" cursor so a page boundary
inside a group of equal mtimes does not drop sessions. The widened
`number | string` contract left the CLI callers that store or compare that
cursor typed as `number`, which is a type error in `tsc --build` even though
nothing changes at runtime.

Widen the three consumers to `number | string`: the daemon's persisted scan
cursor, the live coordinator's paging plus its loop guard, and the session
picker state used by both the ink and opentui pickers. No behaviour change.

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

* fix(cli): route composite session cursors to the persisted listing

The persisted listing mints `"<mtime>|<name>"` cursors when a page ends
inside a group of files sharing an mtime, but the route still chose between
the persisted and the live listing path with a numeric-only predicate. A
composite cursor therefore fell through to the live path, whose parser only
accepts numbers, and the second page answered 400.

Export one predicate from session-list.ts carrying the same shape test the
cursor parser already applies, and use it for that routing decision.

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

* fix(session-list): keep composite cursors usable by ACP and VS Code clients

listSessions mints "<mtime>|<name>" cursors whenever a page ends inside a
group of files sharing an mtime, but two consumers still parsed every cursor
as a number: ACP session/list rejected page 2 with "not a valid numeric
cursor", and the VS Code companion turned the composite cursor into NaN so
hasMore read false and only page 1 was ever listed. A malformed "|name"
cursor also slipped through the persisted-cursor classifier and answered 200
with an empty page instead of invalid_cursor.

- acpAgent: accept and forward "<mtime>|<name>" cursors to listSessions
- session-list: require a non-empty mtime prefix before classifying a
  cursor as persisted
- vscode companion: pass the daemon cursor back verbatim, widen the cursor
  type and treat only a non-empty cursor as "append to the current page"

Tests: ACP composite passthrough + empty-prefix rejection, VS Code
getSessionListPaged composite nextCursor round trip.

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

* fix(vscode): keep composite session cursors type-safe in the companion

021698420c widened the companion's paged session cursor to number|string but
left AcpConnection.listSessions typed number-only, so the package build failed:
TS2345 at qwenAgentManager.ts:573 and TS2365 at :626.

- accept the daemon's "<mtime>|<name>" cursor in AcpConnection.listSessions,
  which already forwards String(cursor) to ACP
- derive the mtime floor from the cursor prefix in the file-system fallback;
  the previous comparison coerced the composite string to NaN and silently
  returned an empty page

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

* fix(session): align persisted-cursor docs, error kind, and tie-break test

- test(core): feed readdir a non-name order so deleting the sort tie-break
  fails the equal-mtime pagination test (mutation-verified: 9/25 without it)
- fix(cli): report a persisted cursor as such instead of "numeric" in
  InvalidCursorError, matching the message ACP already uses
- docs(cli): describe the opaque persisted cursor in the serve protocol doc
- docs(core): drop the mtime-only-cursor rationale this change removed

Declined: folding the cursor codec into one validated helper across core and
the transports — a cross-package API change outside this PR's scope.

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

* docs(session): describe composite persisted cursors on every continuation page

`listSessions` mints `"<mtime>|<name>"` whenever a page has more results, so
the protocol text must not suggest a bare numeric response. The standalone
list route dispatches through the metadata parser, so its invalid-cursor test
should pin the metadata-kind message instead of the persisted one.

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

* test(session): pin the composite persisted cursor shape and narrow its docs

R3-1: the cursor-contract sentence claimed the numeric-cursor input and the
"<mtime>|<name>" reply for every persisted-backed list, but view=organized and
the parentSessionId / sourceType filtered lists page through their own opaque
activity cursors and reject a numeric cursor with 400 invalid_cursor. Narrow
the subject to the legacy recent list and name the other cursor families.

R3-2: no assertion pinned the documented shape. Every persisted-path cursor
assertion was shape-agnostic, so a regression back to a bare numeric mtime
would have kept the suite green. Assert the shape on a page whose boundary
mtime is unique; mutating the mint site to String(lastProcessedMtime) turns
this assertion red.

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

* docs(daemon): name the persisted list cursor in the SDK catalog guide

The batch-cursor sentence kept the legacy "numeric list cursors" wording while
the list route now mints the opaque `<mtime>|<name>` token, so an SDK
integrator parsing the returned cursor got `NaN` and a cursor they could not
send back. Match the protocol doc and point at the persisted list cursors.

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

---------

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-06 15:43:21 +08:00
45f71b3714 fix(ci): apply the apt-mirror probe to the release docker lane too (#16278)
* fix(ci): apply the apt-mirror probe to the release docker lane too

The release docker integration lane builds the same sandbox image as the E2E
leg but never selected an apt mirror, and it prunes the builder cache on every
run so its apt layers are always cold. On mainland pool hosts that rebuilt
against the throttled deb.debian.org path (11-45 kB/s) — the same defect #16214
fixed for E2E, left unfixed here because it was out of scope (#16271).

Extract the probe (apt_mirror_probe_rate / select_apt_mirror) and a new
apply_apt_mirror helper into a shared .github/scripts/apt-mirror-probe.sh,
sourced by both run-e2e-tests.sh and run-release-docker-integration.sh. Each
calls apply_apt_mirror before build:sandbox, so a mainland host builds against
the VPC intranet mirror (6-9 MB/s, verified for E2E) while a healthy default is
left untouched. The Dockerfile APT_MIRROR ARG already exists from #16218.

Contract tests read the shared probe and pin the candidate loop and the source
statement (not substrings the comments also satisfy); both callers must apply
the probe before the build. Mutation-verified: removing the release call, a
candidate, or a source line each reddens the suite.

* fix(ci): carry the apt-mirror probe into the shard-retry test rig

run-e2e-tests.sh now sources its sibling apt-mirror-probe.sh at startup.
The shard-retry meta-test copies only run-e2e-tests.sh into a temp dir, so
the source resolved to a missing file and 'set -e' aborted before any shard
ran — all eight cases saw zero npm calls. Copy the probe file alongside the
script in the rig so the source resolves; sourcing only defines functions
(no network at source time) and the sandbox:none path never calls
apply_apt_mirror, so the rig stays hermetic.

Witness: CI at 315733fe showed all 8 e2e-shard-retry cases failing
(npmCalls expected 2, got 0); with the probe carried into the rig all 8
pass locally. eslint exit 0, prettier clean.

* fix(ci): address #16278 review — hermetic probe test, shebang, scope header

R1-1: the release-lane exec harness now stubs curl (per-URL rates) so the suite
no longer probes the live network; the npm stub logs BUILD_SANDBOX_FLAGS and two
execution tests assert the fast mirror is passed as --build-arg APT_MIRROR with
the CI label preserved, and that an all-slow probe adds no mirror arg. Flipping
the mirror-selection guard reddens them.

R1-2: add the inert shebang so file(1) reports text/x-shellscript and the
shellcheck lane enrols the sourced probe; a lint test pins the mime type.

R1-3: narrow the header to the two covered build sites and record the third
cold caller (release-sdk.yml build-sdk) as out of scope, tracked in #16317.

* test(ci): drive the shellcheck lane for the probe; copy scripts dir in shard harness

R2-1: the shebang test now runs the lane's own candidate selection with only
shellcheck stubbed, so a filter change dropping the probe is caught (removing the
shebang reddens it). R2-2: the shard-retry harness copies the whole .github/scripts
dir so any sourced sibling comes along instead of a hand-maintained file list.

* merge(ci): sync origin/main to pick up the pinned COREPACK_HOME gate

The lint-gate-freshness lane went red on 9b3d131cdb because main moved
.github/workflows/ci.yml at 2b9c4a98f4, and the shard-4 Linux lane died on
the very corepack 'pnpm.mjs' miss that commit fixes.

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

* docs(ci): stop promising the release lane a benefit its routed host cannot get

`integration_docker` is pinned to `ecs-qwen-hk4-host`, or `ubuntu-latest` when
the ECS pool is unavailable. On the Hong Kong host `deb.debian.org` already
answers at MB/s and the VPC mirror is unreachable, so `select_apt_mirror`
correctly keeps the built-in source; on a hosted runner the same is true. The
call-site comment asserted a mainland benefit this call site cannot obtain, and
the always-cold builder cache it sits next to is unchanged.

The comment now says what the call does: apply the same probe the E2E leg uses,
so a host whose default is throttled builds against a faster endpoint, while a
host whose default is already fast keeps it. The `Fixes #16271` claim is
narrowed to `Refs #16271` in the PR body — the always-cold half stays open.

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

* fix(ci): keep the release docker lane script executable

The previous delivery of 6218478b62 went through the Git Data API, which
recorded .github/scripts/run-release-docker-integration.sh as 100644 while
the base has it 100755. release.yml runs these by bare path, so the mode is
load-bearing, and scripts/tests/release-workflow.test.js
> keeps the extracted release scripts executable already fails on it
(Linux unit tests shard 3/8 on 6218478b62).

Restore the recorded mode to 100755.

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

* test(ci): stage the probe fixture at its real path (#16278 R2-1)

The rewritten case kept its fixture at the temp-repo root, where the
lane's `^([^.]+)` candidate alternative already matches, so deleting the
`.*\.(sh|zsh|bash)` arm left it green. Put the file at
`.github/scripts/apt-mirror-probe.sh` — a leading dot makes the `.sh` arm
the only way it can be selected — teach `setup()` to create parent
directories for nested names, and assert the exact shellcheck argv.

Two-arm check on the lane's own run string: as authored, exit 0 with
`... --color=never .github/scripts/apt-mirror-probe.sh`; with the `.sh`
arm deleted, exit 1 ("matched no shell-script candidates").

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

---------

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

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

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

---------

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

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

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

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

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

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

---------

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

lockExecutionControl now decides that under the Session root alone: the pin
lives on the root, and profile slots and bindings require a committed pin.
lockManagedSession and requireProvisionPin lock the root before slot and
binding rows, so every placement owner shares the root-then-binding order
that guard-free dispatch uses. Without an existing root, a locking read takes
only a gap lock and orders nothing; dispatch then keeps the guarded path (a
MySQL stress test reproduced the deadlock between the binding's unique-index
and primary-key lookups otherwise). Admission still takes placement for the
harness drain fence.
2026-11-06 06:25:42 +00:00
callmeYe 1bac51b7d9 feat(sdk): support native Skill archives and receipt lookup (3/4) (#16423)
* feat(skills): add file-backed archive installation kernel

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

* feat(sdk): support native Skill archives and receipt lookup
2026-11-06 04:48:50 +00:00
16794c25ad chore(release): sdk-typescript v0.1.28 (#16412)
* chore(release): v0.26.8

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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Shaojin Wen <[email protected]>
2026-11-06 12:26:37 +08:00
callmeYe da7af136e5 feat(skills): expose native archive routes and receipts (2/4) (#16422)
* feat(skills): add file-backed archive installation kernel

* feat(skills): expose native archive routes and receipts
2026-11-06 12:09:38 +08:00
callmeYe 17ad9cdb04 feat(skills): add file-backed archive installation kernel (#16421) 2026-11-06 11:30:04 +08:00
callmeYe 9b1ce46d77 feat(serve): add atomic user Skill activation (#16186)
* feat(serve): add atomic user Skill activation

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

* docs(serve): index user Skill activation endpoint

* fix(serve): preserve User Skill refresh recovery
2026-11-06 11:18:44 +08:00
qqqys 4cea3a792f feat(cli): have an Ink TUI turn await its shared tool batch (#16385)
With experimental.unifiedAgentLoop on, the stream processor hands a
shared round's tool batch to the round's submitQuery, which awaits it,
its completion step and the round that step sends, once its own stream
and activity ended. While another stream is active, which may be the
stream that sent the round (a response of provider replays alone), the
batch completes on its own as before.

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

The design doc describes PR 3b-2b.
2026-11-06 10:31:47 +08:00
Lanre Shittu 7bd78683e6 fix(core): accept images for Llama 3.2 Vision, Llama 4, LLaVA, Pixtral and Mistral Small 3.1 and 3.2 (#16311) 2026-11-06 02:43:29 +08:00
曹潇缤andShaojin Wen ecc64a735a feat(qqbot): deliver proactive loop messages as active group messages (#15635)
* feat(qqbot): deliver proactive loop messages to the group target

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

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

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

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

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

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

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

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

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

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

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

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

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

* docs(qqbot): record the proactive delivery contract

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

Review round follow-ups for the session title sidecar:

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

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

---------

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

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

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-05 18:09:10 +00:00
Shaojin WenandQwen-Coder 36c8b34222 perf(core): load the MCP client's Google auth providers on demand (#16242)
* perf(core): load the MCP client's Google auth providers on demand

mcp-client imported GoogleCredentialProvider and
ServiceAccountImpersonationProvider statically, so every process that
loads the MCP client, including each resident Managed Runtime worker,
also loaded google-auth-library whether or not any server uses Google
authentication. createTransport is already async; import each provider
only for a server configured with it.

Add transport tests for the service-account impersonation provider,
which had none in this file. On Linux x86_64 an idle worker loads
0.24 MiB less JavaScript and holds 0.9 MiB less private memory.

* fix(core): ask for a restart when a Google auth provider chunk is missing

Now that the MCP client imports its Google auth providers on demand, the
bundle puts them in separate chunks. A background update that replaces
dist under a long-lived process made the next connection fail with
"Cannot find module", wrapped as a connection error for the server.

Move the restart guidance that contentGenerator already gives for its lazy
provider chunks into utils/errors (getModuleNotFoundError,
wrapLazyModuleLoadError) and apply it to both provider imports. The
original error stays as the cause.

Add google-auth-library to the ACP static closure guard, so a static
import of either provider fails check-serve-fast-path-bundle again.

* test(core): isolate the lazy Google auth chunk tests in their own file

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-05 18:08:58 +00:00
Shaojin Wen a32646e640 docs(design): propose PR state transitions from the daemon PR refresh (#16246)
* docs(design): propose PR state transitions from the daemon PR refresh

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

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

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

Two inconsistencies with the channel watch the proposal replaces:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* docs(changelog): sync for v0.26.8

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-06 01:02:08 +08:00
1ac0c6e874 test(web-shell): consolidate sidebar suite fixtures on the shared sidebarHarness (#16016)
* test(web-shell): migrate WebShellSidebar.brand fixtures to sidebarHarness

* test(web-shell): migrate WebShellSidebar.footer-version fixtures to sidebarHarness

* test(web-shell): migrate WebShellSidebar.local-files-footer fixtures to sidebarHarness

* test(web-shell): migrate ChatPane fixtures to sidebarHarness

* test(web-shell): migrate WebShellSidebar.collapse-persist fixtures to sidebarHarness

* test(web-shell): migrate WebShellSidebar.current-session-delete fixtures to sidebarHarness

* test(web-shell): migrate WebShellSidebar.session-pinning fixtures to sidebarHarness

* test(web-shell): migrate WebShellSidebar.workspace-removal fixtures to sidebarHarness

* test(web-shell): use sidebarHarness in App.test.tsx

* style(web-shell): format sidebarHarness.ts

* test(web-shell): migrate StandaloneRecents fixtures to sidebarHarness

* fix(web-shell): repair merged sidebar suites (React import, stable controller mock, listWorkspaceSessionsPage, hook naming)

* fix(web-shell): repair gate2 sidebar failures (createSessionGroup, useStreamingState, stable catalog query identity)

---------

Co-authored-by: Сергей Панченко <[email protected]>
Co-authored-by: 易良 <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
2026-11-06 00:59:26 +08:00
Shaojin Wen ad487ffedb feat(autofix-pr): give helper refusals a code and a next step (#16238)
* feat(autofix-pr): give helper refusals a code and a next step

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

The skill's Stop step and both design docs say what `heldBy` means.
2026-11-05 16:53:30 +00:00
Shaojin Wen a7d0193991 fix(core): keep a writer grant an in-flight renewal refreshed (#16287)
* fix(core): keep a writer grant an in-flight renewal refreshed

reacquireWriter() waits for an in-flight writer renewal before it
acquires, then always dropped the grant and posted the acquire. When
that renewal had answered with a live lease, the fresh grant was thrown
away; until the acquire answered, authorizeLifecycle and any caller
reading the grant without waiting in ensureWriter refused locally with
"the HTTP Managed Session writer is not active." A Store holding the
Session's lifecycle can hold that acquire, so a Hosted Harness refused
lifecycle phases during Spring recovery. HostedShellLifecycleRecoveryIT
[spring-kill] failed this way in 17 of 239 CI runs since #15813 made
renewActivation reacquire a lapsed writer.

Keep the grant when the awaited renewal moved the lease past the lapsed
one and the new lease is live. A refused renewal, or a commit that only
copied the lapsed lease, still reacquires.

* test(core): pin the lease check and renewal timer a kept grant relies on

Two cases join the reacquireWriter tests:

- A refused renewal is held while, after a wall-clock step back, a
  journal read copies the lapsed lease into a new grant object. The
  writer must still be re-acquired. Only the check that the lease moved
  forward guarantees this; a check on grant identity would keep the
  copy.
- The grant is kept, then the activation renewal meets the lifecycle
  claim fence. The timer that the kept grant's renewal re-armed must
  still send the next ordinary renewal before that lease ends.

The reverse case now reaches the lapse before the wait, so the live
check has to read the clock after it. The comment in reacquireWriter
names the journal read among the paths that copy a lapsed lease.
2026-11-06 00:51:40 +08:00
11f6163f0f perf(managed-agent): narrow SSH settlement and worktree locking (#16386)
Settle SSH registrations using their operation and exact physical owner.
Allow ordinary Runtime members and uploads during main worktree preparation
while retaining maintenance ownership and cleanup exclusion.

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-11-05 16:26:28 +00:00
Shaojin Wen b3ad01e875 perf(managed-agent): index the activation lease lookup (#16336)
* perf(managed-agent): index the activation lease lookup

Each activation renewal reads the Session's current activation row,
three times a minute for every resident Session. Renewals update that row
in place, so it is as old as the activation, and the lookup walked back
over every journal transaction committed since: 22 rows after one Turn,
717 after 40.

Add a generated is_activation_record flag for the two activation
operations and an index that leads with the Session, its activation
epoch and the flag, and look the row up by the flag. A simple CASE keeps
H2 from compiling the expression into an IN set bound to the session that
created the table. The CSI application image builds its expected journal
row column by column, so it sets the flag the way the database does.

A MySQL integration test pins the lookup's plan and result with 1,000
and 10,000 later transactions.

* test(managed-agent): pin the CSI image's mirror of the activation flag

The CSI application image rebuilds the journal row a cut adds column by
column, so it computes is_activation_record in Java as the generated
column does. Nothing held the two together.

- WorkspaceCsiApplicationImage.ACTIVATION_OPERATIONS names the
  operations the CASE marks, and V1 points back to it.
- ActivationRecordFlagTest parses the journal table's CASE and requires
  exactly those names and ELSE 0, then checks on H2 that the generated
  value matches the mirror for every name the CASE mentions and other
  operations the Session Store commits.
- The CUT-image test asserts the added row carries "false" for the
  cut's releaseActivation.
2026-11-05 16:16:55 +00:00
Shaojin Wen 76c692815f perf(core): read each Managed message body once per process (#16337)
* perf(core): read each Managed message body once per process

A Hosted Turn rebuilds the Session's history from its message bodies
several times; every rebuild read every body through the Session Store
again, so Turn t made about 8·t resource reads, each an HTTP call and
about five SQL statements on the control plane, serially.

Keep the bodies a projection has read in one byte-bounded LRU shared by
the process (64 MiB). Bodies are immutable and keyed by their store and
full durable ref, so a hit is exactly what the store returned; only reads
fill it, so a body never read still fails the projection when it is
missing. The bound keeps resident Sessions from each holding their whole
history.

* perf(core): keep a Session's own message bodies when it outgrows the cache

A projection reads every body of its Session in commit order, so with one
LRU over all stores a Session whose bodies exceeded the bound evicted each
body just before its next projection reached it and never hit, and flushed
the other Sessions' bodies on every pass.

The cache is now kept per resource store, with the stores ordered by when
they were last read. A store never evicts its own bodies to keep a new one:
an oversized Session keeps the bodies it kept first and reads the rest from
the store. Room for a new body comes from the other stores, least recently
read first, so contending Sessions re-read only what the others displaced.

Adds cases for that, for the shared default cache, for two concurrent misses
of one ref, and for counting a chunked body by its assembled bytes.

* perf(core): bound the message body cache by the memory it holds

- Charge every entry its body plus a fixed overhead (768 bytes, measured
  at 520 to 740 on Node 22), so many small bodies cannot hold several
  times the bound.
- Keep a body that is a view of a larger buffer in an exact-size,
  unpooled copy, so a small body from the HTTP store does not pin an
  8 KiB pool slab.
- Release a store's entries once the store object is collected.
- Once keeping another of a store's own bodies would take them past the
  bound, the store only uses free room: taking room back gained it
  nothing and made the other Sessions re-read their whole history.
- Pin each key field, the production wiring, concurrent misses,
  assembled-byte accounting, the boundaries and the policy in tests, and
  document what a cache hit no longer checks.

* test(core): pin that a cached message body outlives its store copy

A cache hit does not read through the store, so a body already read is
still served after the store loses it, while a projection with a fresh
cache fails. The behaviour is deliberate and documented on the cache;
the case keeps a future change from restoring the store read silently.

* docs(core): say what a cached message body means for readers with their own store

The resource store's read contract does not change: readers that open
their own store read it on every call and fail on a missing referenced
body, while a projection whose cache kept that body keeps serving it
until the entry is evicted or its store is collected. Losing such a
body is damage; nothing removes it while the Session can be read.
2026-11-06 00:14:22 +08:00
Shaojin Wen 5d45b42940 feat(verify-pr): give the sandboxed round the PR's review threads (#16300)
* feat(verify-pr): give the sandboxed round the PR's review threads

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

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

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

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

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

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

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

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

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

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

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

Deleting the shared stub's fail arm, its empty-stdout arm or its argv
capture turns the case red, and so does recording only the first
argument.
2026-11-05 23:50:44 +08:00
Xiongwei Xiaoand易良 0c620f62d8 test(core): harden renewal waits and writer seal coverage (#14482)
Co-authored-by: 易良 <[email protected]>
2026-11-05 23:42:54 +08:00
Shaojin Wen 5239d69cf1 fix(channels): keep GitHub inbound recovery armed after a failed read (#16402)
A corrupt or unreadable inbound task store at connect and at the first
poll's recovery latches the in-memory recoverable-task count at 0. The
recovery flag was cleared in a finally, so a later poll never re-ran
recovery once the store became readable: the persisted task never ran,
and its cursor-blocking state kept the notification window held.

Clear the flag only after a successful recovery pass, matching the
watch-reconcile guard, so a failed read keeps the retry the log already
promises.
2026-11-05 23:39:12 +08:00
易良andQwen-Coder 7c6f93ca51 fix(cli): forward the folder-trust store into the container sandbox (#16390)
* fix(cli): forward the trusted-folders store into the sandbox

The ACP hook tests red only in the docker lane since the folder-trust
feature was enabled in their child home (#16264): the rig grants the
workspace through QWEN_CODE_TRUSTED_FOLDERS_PATH, but start_sandbox never
passed that path through. The container fell back to
<QWEN_HOME>/trustedFolders.json, found no trust decision, and the settings
layer dropped the workspace scope, so the approved project hooks never
reached the registry.

Forward the override like the MCP and hook approvals paths, translating
the Windows path first.

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

* test(serve): allow the trust-store read in the sandbox env guard

The sandbox launcher now reads QWEN_CODE_TRUSTED_FOLDERS_PATH twice while
forwarding it, and the serve process.env guard pins every access. Register
the new read in the launcher's allowance so the guard suite stays green.

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

* test(serve): pin unset sandbox env stores

Cover every forwarded approval/trust store in the unset branch, so dropping a guard cannot silently pass "undefined" into the container.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-05 22:46:14 +08:00
Shaojin Wen cc78e35d5f perf(serve): keep the ACP spawn channel out of Managed Runtime workers (#16235)
The worker sizes media responses with DAEMON_ACP_NDJSON_LIMITS, which
managed-tool-media imported from acp-bridge's spawnChannel. That module
pulls in the ACP SDK schemas, the acp-bridge status module and, through
it, core's package root, so every resident worker loaded about 1.5 MiB
of modules for one constant.

Define the constant in the dependency-free ndJsonStream module,
re-export it from spawnChannel, and import it from there. On Linux
x86_64 an idle worker drops from 91.0 to 79.6 MiB of private memory and
from 215 to 175 loaded modules.
2026-11-05 14:42:05 +00:00
c8eaa04e81 test(review): pin the carried-ledger carrier order and fail-open arms (#16395)
Four test-only gaps in dedup-candidates.test.ts; no production change:

- a claim both carriers hold is dropped once and cites the POSTED
  carrier, not the artifact's mirror of it (carried is posted-then-
  deferred), with both halves asserted loaded so the order is what
  decides the citation
- the side-file read fails open on its own two arms, which the absent-
  file case does not reach: a truncated JSON file lands in the parse
  catch, and a non-array `findings` / `round: 0` file is refused by the
  shape guard — no throw, no drop either way, each with a control that
  the same claim DOES drop when the file is intact
- a kept candidate reaches kept[0] with every extra field verbatim, so
  the Step 4 verify shards can still be built from the kept list
- the deferral half's three keep arms (far anchor, loose overlap,
  Critical candidate vs non-Critical entry) each keep, with a control
  that the same pair still absorbs under matching input

Every new case was verified to have teeth: each was pushed red against a
mutated production behaviour (carrier order swapped, parse catch
rethrowing, each shape-guard leg removed alone, kept narrowed, anchor
tolerance widened, similarity bar removed, severity guard removed) and
the production file was restored byte-identical afterwards.


Patrol-Run: qwen-issue-patrol/jmw3k5nxoy7

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-05 22:41:53 +08:00
Shaojin Wen ef6914c1d9 fix(channels): keep GitHub replies hit by a secondary rate limit (#16334)
* fix(channels): keep GitHub replies hit by a secondary rate limit

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Three review findings on the update restart path.

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

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

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

The workspace-qualified hooks status GET sat behind the trusted-runtime
gate alongside env/preflight, so with folder trust enabled an untrusted
workspace got 403 exactly when the Web Shell approval card needed the
pending decision. The card treats the failed read as no hooks and never
renders, leaving no interactive way to approve. Route the hooks read
through the same trust-free resolver as the qualified approve POST;
env/preflight keep the trusted gate.

Fixes #16387

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

* test(serve): pin pending hooks decision for untrusted workspace

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-05 22:00:59 +08:00
callmeYe 2be58e52ee fix(cli): preserve raw context usage estimates (#16360) 2026-11-05 13:33:50 +00:00
liuxuely0626andluoyun 0cc37bf444 feat: add Qwen subscription support across CLI, VS Code, and Desktop (#16378)
* feat(subscription): add development login, models and web search

* feat(subscription): refine account flows and host integration

Align desktop and VS Code subscription UI, add account logout and confirmed plan display, and map subscription thinking parameters. Remove simulation code and obsolete native authentication paths, consolidate design documentation, and cover cancellation and recovery boundaries.

* fix(subscription): preserve request errors and remove unused compatibility

* fix(subscription): refine command visibility and client logout flows

* fix(subscription): simplify account actions and desktop sign-in feedback

* docs(subscription): clarify sign-in and provider guidance

* feat(subscription): support switching accounts across clients

* fix(subscription): align account actions and open plans on demand

* fix(subscription): restore model selection after logout

Filter subscription-only commands consistently across terminal renderers, preserve model catalog error classification, and display the subscription authentication name consistently.

* fix(subscription): unify status authentication labels across clients

* fix(subscription): preserve provider connections across client logout

Restore configured providers after subscription logout across CLI, Desktop, and VS Code. Keep unrelated connections and conversation history, synchronize model state silently, and return to an unconfigured chat when no provider remains.

* fix(subscription): correct native client logout state

* feat(subscription): enable sign-in without development mode

* refactor(ui): 优化会员登录成功提示和界面交互体验

- 移除订阅欢迎提示相关状态和行为,简化 UI 状态管理
- 替换登录成功提示为四行摘要,包含邮箱、套餐、模型及常用命令提示
- 更新国际化文本,支持新的登录成功摘要内容
- 取消订阅模式下输入框下方的命令提示行,改为在登录成功摘要中展示
- 移除多余的测试用例,新增登录成功消息的单元测试校验
- 优化 AppHeader 组件,不再显示订阅启动重复提示
- 调整 Composer 组件逻辑,删除订阅提示相关副作用及渲染条件
- 修改历史消息显示组件以适应新的成功提示格式

* fix(subscription): enable Flash reasoning controls in shared UI

* fix(subscription): prioritize authorized models and simplify login dialogs

* fix(subscription): preserve gateway errors and request IDs

* fix(subscription): synchronize account state across clients

Preserve model selections after sign-out, synchronize account commands and model catalogs, and retain explicit browser sign-in flows across CLI, Web Shell, and VS Code. Cancel subscription background agents on logout and normalize expired refresh credentials without SDK error wrapping.

* fix(subscription): reset default model on explicit sign-in

Choose the default served model when explicitly signing in to Qwen Subscription, preferring Max over a previously saved Flash selection. Preserve normal startup behavior and other provider flows, and cover CLI and workspace activation with regression tests.

* fix(vscode): refresh models after successful sign-in

* fix(web-shell): hide account actions during automatic sign-in

* fix(web-shell): align model details and auth keyboard navigation

* docs(subscription): revert previous guide changes

* fix(core): reuse subscription catalog for same-login model switches

* docs(subscription): document Qwen membership sign-in

* fix(subscription): unify quota notices and preserve provider switching

Carry structured subscription quota details through chat and search errors for localized CLI, Desktop, and VS Code display. Keep other providers available when subscription discovery fails and refine sidebar account actions.

* fix(web-shell): align Chinese subscription entry title with CLI

* fix(subscription): open plans for saved non-member accounts

* fix(web-shell): show subscription commands after desktop sign-in

* fix(subscription): reauthorize explicit account sign-ins

* fix(auth): add model switching hint to subscription success

* docs(auth): list Qwen OAuth as the first option

* docs(models): simplify subscription sign-in guidance

* docs(models): remove legacy OAuth from supported auth types

* docs(subscription): link membership references and remove obsolete allowance note

* fix(subscription): preserve session until reauthorization succeeds

* fix(subscription): use pre for browser authorization and plans

* fix(subscription): use production endpoints

* test(vscode): fix editor message test formatting

* fix(subscription): preserve unauthenticated session startup

* fix(cli): classify subscription auth during ACP preflight

* fix(subscription): scope activation to the current project

---------

Co-authored-by: luoyun <[email protected]>
2026-11-05 20:44:21 +08:00
qqqys 08f1843c22 feat(mods): run the prompts a Mod submits in the Ink terminal (#16373)
* feat(mods): run the prompts a Mod submits in the Ink terminal

* test(mods): run a Mod's prompt in a real Ink terminal

* fix(mods): stop a Mod's prompts when the person stops a turn in Ink

- Esc or declining every tool a turn asks to run drops the Mods' queued
  prompts and refuses new ones until the person sends a prompt again. The
  stopped turn's turn.complete hooks run after the stop, and a call a hook
  does not await reaches the host after the hook returns, so dropping the
  queue alone let such a hook start the next turn. $.turn.abort still keeps
  the queue.
- A Monitor update waiting out its minimum interval no longer holds a Mod's
  prompt, which a Monitor printing within every interval held for good.
- A prompt put back after losing the idle moment runs once the winning
  submission settles, since that one may start no turn.
- The dual-output sidecar gets the Mod's text and a mod_prompt system
  message, as a stream-json client does, instead of the frame.
- The guide's capability table lists Ink for a Mod's turn events.

* fix(mods): drain Mod prompts after any settle and strip a stopped one

A Mod prompt queued while a submission that starts no turn held the
session (a cron's Mod command, a notification dropped before sending)
stayed queued with the session idle: the drain re-ran on settle only for
a prompt it had put back. Re-run it whenever a Mod prompt waits.

When the person stops a Mod prompt's turn before it answers, the framed
prompt stayed at the end of the model history, so the next prompt reached
the model behind it. The cancel info now says the turn ran a Mod's
prompt, and the handler strips it as it does a Goal continuation's.

* fix(mods): run a waiting Mod prompt once a Goal lets go of the turns

A Mod prompt queued while a Goal owned the session's turns waited for the
next submission to settle or another prompt to be queued. The Goal often
releases its turns after the session is already idle (its verifier accepts,
its last turn is released), so the prompt could sit until some unrelated
event. The drain now also re-runs on the Goal runtime's broadcasts while a
Mod prompt waits.

The test no longer queues a second prompt to wake the drain; it changes the
Goal snapshot and broadcasts, and fails without the fix.
2026-11-05 20:39:00 +08:00
易良andQwen-Coder 4e75789d8e test(web-shell): pin the rewind capacity-latch re-open (#16358)
* fix(web-shell): defer rewind capacity-latch re-open publish past the rebuild commit (#16347)

A rewind drops the newest blocks, and the rewind rebuild that follows
replays the surviving prefix synchronously. Publishing the capacity-latch
re-open synchronously inside the truncation callback let a concurrently
batched same-anchor page append land before the rebuild's prefix, which
macOS CI observed once as a block-order inversion in
DaemonSessionProvider's 'reopens a rejected history page after
authoritative rewind with same anchor'.

Keep the latch ref updates synchronous (loadMore gating and synchronous
readers still see the reopened latch immediately) and defer only the
React state publish one microtask, past the rebuild commit. Oldest-first
trims keep the fully synchronous path.

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

* test(web-shell): pin the rewind capacity-latch re-open and correct the fix comment

Addresses two review findings on the #16347 fix:

- The deferral was unpinned: no test could distinguish the deferred publish
  from the previous synchronous one. Add a case that latches the capacity
  rejection, commits a rewind rebuild, and asserts the latch re-opens once
  the rebuild frees the window. A regression that drops the truncation
  reconciliation on the rebuild's store.reset() turns this red while the
  pre-rewind latch assertions stay green. The deferred-vs-synchronous
  distinction itself is not observable through React act() microtask
  flushing, so the test documents that it pins the re-open contract, not
  the queueMicrotask mechanism.

- The comment described the rebuild as following the publish; the rewind
  reducer installs the surviving replay prefix BEFORE invoking the
  truncation callback. Restate the ordering against the store commit and
  the notify microtask.

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

* test(web-shell): pin the rewind capacity-latch re-open

Drop the one-microtask deferral of the rewind latch publish: the
truncation callback runs from the SDK reducer after state.blocks is
sliced, and the publish carries only latch metadata, so a microtask
cannot reorder any store-level step. The added test stays - it pins
that a rewind rebuild re-opens the capacity latch and the rebuilt
window keeps only the surviving prefix.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-05 19:51:00 +08:00
qqqys 0750e8bd02 fix(core): tell the model the date once, from the history (#16362)
* fix(core): tell the model the date once, from the history

The date reminder was deduplicated by an in-memory field that startChat
reset and that never knew about the date the startup context carries. Every
session's first turn repeated the date with a note that the two might
differ, a rewind after midnight could leave only the stale startup date, and
cron turns never refreshed it.

The client now finds the last date the history gives the model (the startup
context, including the legacy prelude, or an earlier date-only reminder in
either wording) and adds a reminder on user and cron turns only when that
date is not today: "Today's date is X." when there is none, and a short
"the date has changed" note otherwise.

Closes #16359

* fix(core): read the announced date from a marker on the date reminder part

The history scan matched date reminder text anywhere in a user part, so a
reminder the user pasted or @-included could stand in for the client's own:
a pasted block dated today suppressed the real date-change reminder, and an
older one triggered a false "the date has changed".

The date reminder part now carries its date as part metadata, which never
reaches the model, and the scan reads only that marker, the startup context,
and the pre-marker wording as a whole part. Tests cover pasted reminders,
a saved-record round trip, and the reminder the client sent being read back
on the next turn; the tool-response IDE test sends a ToolResult turn again
and keeps its strict no-reminder assertion.

* fix(core): tell the date after a resume, compaction or retry

A startup context rebuilt in front of earlier turns (resume, compaction)
gave today's date, so the client sent no reminder although those turns
may have run under another date. The startup context now records its
date as part metadata only when it starts the conversation; a rebuilt
one records none, and the next turn tells the date with a note that
earlier parts may come from another date. A refresh keeps the date the
old startup context recorded. A retry tells the date again when the
failed entry it drops had told it.
2026-11-05 19:50:15 +08:00
c0782a8cb6 test(ci): pin the zero-skip timeout shape in awaitTwoSkipLines (#16384)
Refs #10737


Patrol-Run: qwen-issue-patrol/jmw3fvcf8y3

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-05 11:45:43 +00:00
易良andQwen-Coder 6fe5bca8e1 test(managed-agent): match the busy-storage message in ChildWorkspaceServiceTest (#16382)
`WorkspaceExecutionStore.busy()` changed its message to
"Workspace is not available for this operation." in #16369, which updated the
in-test stub at line 1345 but not the `aBusyStorageParksTheStepWithoutSpendingAnAttempt`
assertion at line 707. The assertion still expected the old "held by another tool
turn" text, so the test now fails deterministically on the new message (seen on
run 41981102231, both attempts). Update the assertion to the new busy message.

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-05 19:30:31 +08:00
Shaojin Wen 481bd948de feat(web-shell): link channel event rows to their sessions (#16375)
* feat(web-shell): link channel event rows to their sessions

Operators investigating a bound-session delivery or a published reply had
to copy the session id out of the journal and find the session by hand.
Journal rows that name a session now offer an "Open session" button on the
Channel events page, reusing the existing in-app session navigation path.

GitHub task rows previously journaled no session at all, even though the
bound delivery target is known at every transition. They now record the
target session and, from the binding snapshot, its owner workspace, so
cross-workspace targets open correctly; outbound rows additionally stamp
the channel's own workspace. The new journal/SDK field is optional: older
records fall back to the journal's workspace, which is correct for the
channel's reply session, the only session kind older records name.

* docs: correct the locked-workspace claim in the session-link design

The open-session handler's locked-workspace guard only applies to
browser-notification clicks dispatched on a dedicated EventTarget;
window-dispatched in-page links (markdown session links, and the new
events-page button) are not subject to it. The design said the jump
"honors locked-workspace mode" and that locked shells "ignore
cross-workspace jumps" — both inaccurate. State the observed behavior:
in a locked shell the button behaves exactly like an existing markdown
session link.
2026-11-05 18:09:28 +08:00
qqqys 5d5591ed91 feat(cli): end the Ink TUI's shared tool batch on the TUI's own stops (#16365)
* feat(cli): end the Ink TUI's shared tool batch on the TUI's own stops

With experimental.unifiedAgentLoop on, the shared tool batch ends only
when no model stream is active, so its decisions see the session as the
continuation will. A quota model switch and a background agent launch
into the last background slot now end the batch through its result
callback, and the completion step stops on the batch's reason. A tool
that ends the turn ends it outside a Goal turn too, with its results in
history, as on the other surfaces.

Part of #15327 (PR 3b-2a).

* fix(cli): drain the Ink TUI's held shared batches in one continuation

Shared batches that settled during a model stream were resumed together
when it ended, and each started its own continuation. The drain now waits
for the batches it resumes, which join its queue meanwhile, so their
results go out in one continuation and the submission holds the turn off
Idle until then.

- A quota model switch now also stops a deferred shared batch, the
  background-slot check still covers scheduler-run calls drained with a
  shared batch, and a model switch wins when stops merge.
- A model-switch stop outside a Goal turn writes the results to history.
- A rejected onSettled promise is logged.

* fix(cli): check the Ink TUI's batch stops again when the results are sent

A shared batch decided its background-slot stop when it ended, so a batch
deferred again behind a new stream kept a stale stop after a slot freed.
The batch now notes which executed calls launched a background agent, and
the completion step checks the slots when it sends, as the current path
does.

- Drop the stored model-switch stop: the completion step reads the switch
  as it is then.
- With the setting off, a quota model switch outside a Goal turn leaves
  history alone again.
- Resume held batches one at a time in the order they settled, so their
  merged results go out in that order.
- Check the tool name before the background registry.
2026-11-05 17:49:15 +08:00
Shaojin WenandQwen-Coder 2feb29c856 fix(cli): stop Agent Host disconnects being undone and report unsaved joins (#16156)
* fix(cli): stop Agent Host disconnects being undone and report unsaved joins

`DELETE hosts/connect` stopped the connection before removing its saved
record. When the removal failed, for example on a held lock, the route
answered 400 after aborting the running turns, and the restore loop
reconnected the still-saved record a few seconds later. It now removes
the record first and stops the connection only after that. A failed
removal stops nothing: a held lock answers 503 for the caller to retry,
and any other failure answers 400, unless the connection has no record
at all (joined at boot, or a join that was not saved), which has nothing
to come back from and is stopped anyway.

`POST hosts/connect` swallowed a failed save of the connection record and
answered as if the join would survive a restart. It still answers
success, because the enrollment token is spent by then, but adds
`saved: false` and `saveError`, unless an earlier record for the same
connection still stands. `qwen agents join` and the web shell's
add-runtime dialog warn about it instead of saying the daemon reconnects
after restarts.

The dialog's two join forms now have their own keys. React reused one
form's inputs for the other, so an enrollment token typed on one tab
showed up in a visible field of the other.

Refs #16136

* fix(cli): tell an unreadable connections file from an empty one on disconnect

Address review R1-1 (Critical) and R1-2/4/5/6/7/13:
- R1-1: readAgentHostConnections mapped every non-ENOENT failure to [], so a
  disconnect whose removal failed (EMFILE/EIO/mode change) read as 'no record',
  stopped the turns, and let the restore loop reconnect the record still on
  disk. Split a strict reader (ENOENT is empty; unreadable/corrupt throws) from
  the resilient one the restore loop keeps, add findAgentHostConnectionChecked
  (found/absent/unreadable), and make DELETE stop only on a confirmed absent
  record, refusing on unreadable.
- R1-2: widen the connections lock retry budget (~0.65s -> a few seconds) so
  transient contention is not reported as a permanent lost join.
- R1-4: the restore loop's keep() now uses findAgentHostConnection instead of
  duplicating the still-saved predicate.
- R1-5/R1-6: the add-runtime dialog clears the unsaved notice before join
  validation and on tab switch, so a stale notice cannot sit beside another
  form's error.
- R1-7: pin a saved remote-connect control, the empty-saveError notice ('.'
  fallback), and the tab-switch clear.
- R1-13: session-multi-agent design §3.5 now states reconnect is conditional on
  the record being saved (both languages).

Tests: cli agent-host 30 pass, web-shell dialog 12 pass; typecheck, Prettier,
ESLint clean. Critical and tab-clear mutation-witnessed (module lookup swallow
and tab-handler revert each redden their pin).

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

* test(cli): pin the disconnect Critical with a real route + real store

Add a real-scenario integration test: the real registerAgentHostRuntimeRoutes
wired to the real agent-host-connections store over a real temporary
filesystem (only the Agent Host client and program probe are substituted).
A readable record is forgotten and stopped; a file made unreadable (a
directory standing in for EMFILE/EIO) makes DELETE refuse with 400 and never
stop, so the restore loop cannot undo the disconnect. Mutation-witnessed end
to end: swallowing unreadable as absent reddens the real-route test.

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

* fix(cli,web-shell): keep Agent Host joins through a slow save

Found by running two real daemons (a coordinator and a Host) on one
machine against faults on the Host's connections file, and in the second
review round.

Connections file:
- The lock used proper-lockfile's own retries, about 3.75 s in all, for
  every error. A holder killed while it held the lock (SIGKILL, OOM)
  leaves it until it goes stale, 10 s later, so a join that met it
  answered `saved: false` and a disconnect answered 503. Writes now retry
  only while the lock is held or changing hands (ELOCKED; ENOENT; EPERM
  and EBUSY on Windows), for up to 12 s, and fail at once otherwise.
  `stale` stays at the default, now spelled out, so daemons of other
  versions agree on it.
- Writers in one daemon take turns, so two of them cannot both take over
  the same stale lock; one still queued 1.5 s past its deadline gives up.
  A writer whose lock was reported lost, or that held it for 8 s, writes
  nothing, and a lost lock is logged instead of taking the daemon down.
- The file is read strictly inside the lock: a read error (EACCES,
  EMFILE, EIO) or a file a newer version wrote fails the write instead of
  replacing every other record. A corrupt file is still replaced.
- DELETE `hosts/connect` also answers 503 for a lock lost before it
  wrote, and 400 with the reason for a file a newer version wrote,
  saying that the connection ends when this daemon stops.

Deadlines: the save can now wait about 13.5 s, so the coordinator gives a
remote's `/connect` 90 s instead of 20 s, the web shell gives
remote-connect 120 s and its own join 90 s instead of the SDK's 30 s, and
`qwen agents join` waits 90 s instead of 60 s. A CLI join that timed out,
or whose connection dropped after it was sent, now says the join may
still complete instead of saying that no daemon answered.

Add-runtime dialog and Agents page:
- Each machine's "not saved" notice has its own slot and shows only with
  the form or result it is about, so a tab switch neither clears it nor
  moves it under the other machine's form. A join's error likewise stays
  with the join form; one a sent join came back with also shows, labelled
  as this computer's join, over another machine's connected result. A
  close drops only the notices and errors on screen.
- A join or remote connect in flight keeps the dialog busy across a close
  and a reopen. Meanwhile the page keeps New agent, Role templates,
  Mention in chat and creating an agent on a runtime unavailable, since
  leaving it would drop the result. Adding and replacing a runtime use
  separate dialogs, so a replacement does not remount a dialog whose join
  is in flight.
- A connected result waits while a request is in flight instead of
  replacing the form that sent it, and a close keeps one that a joined
  result covered.
- A sent join clears its token, so a spent token cannot be sent again,
  and a join that timed out says it may still be joining. The form that
  sent a request is read-only until it settles. A plain-HTTP
  link off loopback without "Allow plain HTTP" is refused before it is
  sent, as the daemon would refuse it, so its token stays.

The design doc now defines `saved` by outcome.

Refs #16136

* fix(cli,web-shell): say why a disconnect stops nothing, and what helps

From the third review round.

Connections file:
- Reading it now says why there are no records: a read error, a file a
  newer version wrote (ENEWERSCHEMA), or a corrupt file (ECORRUPT).
- The restore loop reads a newer version's file as none and says so once
  per file, instead of logging it as unreadable every 5 s.
- A disconnect refused because the file cannot be used now says why
  and what helps: the read error itself instead of "Retry the
  request.", which does not help a permission problem; for a corrupt
  file, "Fix or remove that file, then retry." and why forgetting the
  connection failed; and for a newer version's file, that the
  connection ends when this daemon stops. A corrupt file is still not
  taken for "no record": a real run showed that a file read through a
  mount being replaced comes back empty, and the restore loop
  reconnected from it once the mount was gone, undoing the disconnect.

`qwen agents join`: the daemon answers 2xx only once the join went
through, with `connected: true`, so any other 2xx (cut off, not JSON,
JSON `null`, no `connected`) now says the join may still complete,
worded by whether the answer was cut off or did not confirm the join.
A JSON `null` answer no longer crashes it.

Agents page: while a join or remote connect from the runtime dialog is
in flight, a status line under the title says why the page's actions
are disabled.

Tests: the remote-connect double answers each leg as its handler does,
and the real-store disconnect case checks that the record left the file.

* fix(web-shell): keep a remote's notice, and the join command made meanwhile

From the fourth review round.

Add runtime dialog: creating a join command while a remote connected
from "I have its address and token" had not shown up yet replaced that
wait. Once the remote showed up, it had lost its "could not save the
connection" notice, and closing its result threw the join command away.

- The wait keeps the remote by the workspace it answered from
  (`workspaceCwd`, which the roster also reports for its runtime), and a
  join command created before the remote shows up keeps it too. Only a
  runtime serving that workspace is the remote and carries its notice.
  While the dialog waits for the remote alone, no other computer is the
  one it waits for. With a join command pending too, results come in
  roster order.
- Closing a result keeps waiting for what is left: the remote, after
  another computer's result; the join command's computer, after the
  remote's, unless the command expired and nothing else came.
- An external runtime online before the connect that serves the same
  workspace may be the remote, back under its saved id. Then nothing
  waits on for it.
- An answer without a workspace (an older daemon) is read as before.
- A remote connect drops a join command shown before it: the
  coordinator keeps one enrollment token, and the connect issues its
  own, so that command no longer works.

Tests: the real-store disconnect case checks, through the strict
lookup, that the record left a file that still reads and that another
workspace's record stayed. The lenient lookup reads a corrupt file as
no record.

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-05 01:42:49 -08:00
Shaojin Wen bff144a528 fix(channels): keep GitHub notifications unread on transient thread errors (#16333)
* fix(channels): keep GitHub notifications unread on transient thread errors

A notification whose comments, events or issue could not be read was
skipped while the same poll still marked every notification read and
advanced the cursor, so a GitHub 5xx, network error or rate limit on one
thread lost its @mention for good, without a timeline entry.

Keep the cursor and the unread mark when a thread fails transiently, so
the next poll reads it again. Permanent answers (404, a SAML-protected
403) still skip the thread at once, and a thread GitHub keeps failing is
skipped after 30 polls so it cannot hold mark-as-read back for every other
thread. First-contact issue fetches follow the same rule, and every such
failure is recorded in the channel event timeline.

* fix(channels): keep exhausted GitHub retry streaks until the window commits

Dropping a thread's failure counter when it reached the cap re-armed the
same unread notification whenever another thread still held the cursor:
two threads that started failing one poll apart kept resetting each other
and the window was never marked read. Keep the exhausted streak until the
window commits or the thread succeeds, so the hold ends once every failing
thread has recovered or been skipped, and say so in the docs.

* fix(channels): separate shared GitHub limits from per-thread failures

Treat a rate limit or credential failure on one thread as what it is, an
answer every thread would get: hold the window and stop reading the rest
of the poll, without charging the thread's 30-poll budget, so a limit is
not hammered once per notification every poll. Only a 5xx (Octokit also
reports network errors as 500) counts against a thread's budget. A local
exception without a status skips the thread again, as before, which
keeps the inbound-task persistence guards observable on their own.

A held window re-reads failing threads every poll, so report a streak in
stderr and the event timeline when it starts, every tenth retry and at
give-up instead of on every poll. Pin the shared-limit stop, the
local-exception skip, the reset after a success and the clear on commit.

* fix(channels): bound the GitHub notification hold per window, not per thread

- A success reset a thread's 5xx streak while the window stayed held, so
  two threads failing in turn held mark-as-read and the cursor forever. The
  30-poll budget now belongs to the window: on the 30th poll that sees a 5xx,
  the threads still failing are skipped and the window can be marked read.
  Rate-limited polls do not spend it.
- A held window re-reads every thread and comment on each poll. Permanent
  failures and given-up threads are journaled once per window, rate limits on
  their first and every tenth stopped poll, and the duplicate and
  mention_required skips of a re-read comment once per window.
- Tests stub the first-contact issue fetch they fall through to, so they no
  longer pass only in file order.

* fix(channels): spend the GitHub 5xx budget only on a window that can commit

- A thread left unread at the cursor (its mark-as-read never applied) with
  nothing newer to mark read held nothing, yet each failing poll spent the
  window budget, which then never reset; the next new notification was
  skipped on its first failing poll and its comment lost. Only a window with
  something to commit now spends the budget.
- An error a failing task store raises inside a thread is journaled as
  retried, not skipped: the store failure holds the whole window and the
  thread is read again.
- Pin the 500 boundary (Octokit's network-failure status) and the stderr
  throttle; list github_api_error among the Channel events skip reasons.

* fix(channels): read only failed or changed threads while a GitHub window is held

- A held window re-read every listed thread on every poll, so one failing
  thread cost a read of the whole window per poll, and a secondary limit set
  off by that volume stopped every poll at the same thread: the window could
  neither commit nor reach the notifications behind it. A thread read without
  error is now remembered with its notification's updated_at until the window
  commits; it is read again only once new activity moves that timestamp, so a
  hold re-reads the failing threads and a poll stopped by a rate limit
  resumes where the last one stopped. A failing task store or audit keeps the
  thread unremembered.
- A notification listed again at or before the cursor (its mark-as-read did
  not apply) has nothing new to read; a failure reading it no longer holds
  the window or spends its budget.
- The once-per-window journal keys stop growing at their bound instead of
  clearing, so only the overflow repeats.
- Tests pin the budget surviving a failed commit, a 403 whose only signal is
  retry-after, and two repositories with the same issue number.

* fix(channels): journal each GitHub thread outcome under its own window key

- A retried store failure, a stale notification's skip and a real skip
  shared one once-per-window key, so whichever came first silenced the
  others: a thread could be skipped, and its comment lost at the commit, with
  the timeline still showing only the earlier row. Each outcome now has its
  own key.
- Only a local error (no HTTP status) counts as held by a failing task store
  or audit; a 404 on another thread in that poll is a skip.
- A permanent answer or a stale notification reads the same next poll, so
  the held window remembers it; a thread it gave up on is still read again.
  Stale failures no longer count toward the thread's failure milestones, and
  the per-thread counts share the per-window bound.
- A stale notification that hits a rate limit still holds the window (the
  threads after it were not read); the doc now limits the stale rule to 5xx.

* fix(channels): read cleanly read GitHub threads again on every held poll

Skipping a thread that read cleanly while its notification timestamp stayed
put lost data: the comment lane defers a first-contact body to the re-read
after a failed comment turn, so the body was never fed; and with GitHub's
one-second timestamps or a late notification, an unchanged updated_at does
not prove the thread has nothing new, so a comment posted after the read was
missed before the commit. A held window is read in full on every poll again,
as on main; only threads that failed with a permanent answer or were stale,
which read the same next poll, are not read again until the window commits.

* fix(channels): read a GitHub thread again after an error without a status

A thread whose read ended in an error without an HTTP status was settled for
the held window like a permanent answer. Such an error can come from the
channel's own state part-way through the thread: an unreadable
pending-deliveries file throws after the first comment was claimed, with no
persistence flag set, so the later comments of that thread (or its comment
lane after a direct dispatch) were never read while the claimed task held
the window, and were lost at the commit. Only permanent HTTP answers and
stale notifications are settled now; any other failure is read again, as on
main.

* fix(channels): read every GitHub thread again on each held poll

Settling a thread for the held window after a failed read lost items main
delivers. A stale notification is not empty: a late notification at the
committed cursor's second can carry a first-contact body or a direct event,
neither bounded by the cursor window, and its read can fail on one poll and
succeed on the next. A permanent HTTP answer can be short-lived too, such as
the 404 of an issue that was just created. Every thread of a held window is
read again on each poll, as on main; a stale notification's 5xx still neither
holds the window nor spends its budget.

* docs(channels): say what the GitHub failed and skipped timeline rows mean

The journal contract listed inbound rows as accepted or skipped only; GitHub
thread read failures now add a failed row (the window is held and the thread
is read again) beside the skipped one (the channel stopped holding the
window for it). Name both in the protocol reference and the channel guide.

* fix(channels): share the GitHub rate-limit test with the watch pass

A secondary rate limit sent as a bare 403 (quota left, no retry-after)
stopped the notification poll but not the PR watch pass, which kept
issuing one rejected pulls.get per watched PR. The limit's wording now
lives in the shared predicate, so both stop on the same answers.

A failing thread whose count no longer fits the per-window bound was
reported on every poll; it is now reported once per window. A test pins
that the hold budget starts again after a restart, and the journal
helpers in the test file share one reader.

* docs(channels): say what a github_api_error row means, not only its cause

A failed row also comes from a local error while channel state cannot be
persisted, the journal schema now lists the reason on inbound failures,
and a rate limit stops a held window's poll at the first refused thread
rather than reading it in full.

* fix(channels): report a GitHub thread its own task still holds as retried

A local error inside a claimed task's run, such as an unreadable
pending-deliveries file, leaves that task running: the cursor waits for
it and the thread is read again, yet the timeline called the failure
skipped. The poll now checks the thread's own tasks, as the store was
last read or written, and reports such a failure as failed until the
task settles.

The restart case now saves the cursor and loads it in a fresh instance,
as a real restart does, so a hold count kept in the cursor would fail it.

* docs(channels): keep the task store wording for the guide's first hold exception

* docs(channels): state the bounds of the GitHub held-window journal rows

A skipped row for a notification listed again at or before the cursor
normally loses nothing, since its activity was read before an earlier
window was marked read. The design page now lists the held-window causes
as examples, including an audit it cannot read and a failing
mark-as-read. The once-per-window rows are tracked under up to 8,192
keys, which the guide, the protocol page and both design pages now say,
and a credential failure is journaled like a rate limit.
2026-11-05 17:22:27 +08:00
Jiankai XiaoandJiankai Xiao de6ccb4e3c feat(serve): allow configuring the Conversations working-directory root (#16105)
* feat(serve): allow configuring the Conversations working-directory root

* test(serve): register Conversations root E2E in no-AK CI

* fix(serve): address Conversations root review feedback

* fix(serve): clarify Conversations root configuration errors

---------

Co-authored-by: Jiankai Xiao <[email protected]>
2026-11-05 16:53:05 +08:00
Shaojin WenandQwen-Coder e4c570a387 feat(channels): edit bound-session action templates on demand (#16349)
* feat(channels): edit bound-session action templates on demand

The Web Shell channel editor rendered every boundSessionActions key as its own single-line input — 80 rows for GitHub's closed vocabulary — with one dense paragraph as the only explanation and no way to enlarge the dialog. Operators typically configure one or two keys.

Record fields now show only configured entries and add keys through a grouped picker whose options carry per-key descriptions; values edit in multi-line text areas with explicit delete buttons. GitHub's descriptor supplies the descriptions and role groups, mirrored through new optional option metadata on the channel-base and SDK descriptor types. The channel editor dialog gains a fullscreen toggle, and the templates field moves into the bound-sessions panel with the same delivery gating as watch events. Empty values stay editable in the draft and are filtered by the existing save serializer, so the draft/save contract is unchanged.

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

* fix(web-shell): generic record copy and smoke-tag the template editor E2E

Review follow-up on #16349. The shared record renderer's empty state and add-picker placeholder were template-worded, which mislabels the options-less messageRoutes field; the defaults are now entry-worded and the GitHub/GitLab template fields override them through their field label keys. A new unit case pins the options-less path (generic copy, no picker, stored rows still editable). The new channels E2E case gains the @smoke tag so the CI smoke job actually runs it.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-05 16:48:27 +08:00