Commit graph

1728 commits

Author SHA1 Message Date
probe
54a64e4b75 fix(web-shell): retain localized workspace error guidance 2026-09-10 04:32:34 +08:00
probe
e745c20ac5 fix(web-shell): preserve discovery errors and stabilize history smoke 2026-09-09 21:44:10 +08:00
Shaojin Wen
c3325d274f
Merge branch 'main' into codex/fix-web-shell-cold-session-loading 2026-09-09 16:12:24 +08:00
qqqys
3a262e0914
feat(ipc): drop peer messages that arrive faster than a session can take, and tell the sender once (#11277)
* feat(ipc): drop peer messages that arrive faster than a session can take, and tell the sender once

The inbound gate decided whether a peer message may act. Nothing decided
how fast messages may arrive, and everything downstream costs something
per message: the hold buffer evicts its oldest entry per arrival, so a
flood walks a user's real backlog out one message at a time; the input
queue fills; and every outcome draws a receipt over an outbound ceiling
shared with everything else the session sends, so the receipts most
likely to be lost are the ones belonging to the legitimate messages
arriving alongside the flood.

Meter arrivals before policy runs. Thirty from one sender at once, then
one every two seconds; sixty across all senders, then one a second. The
second limit exists because a sender names itself on the frame, so
rotating that name buys a fresh allowance from the first limit but not
from the second. Neither is a security boundary — a hostile same-uid
process has better options than flooding a socket — they bound the
damage an ordinary bug does, such as two same-class sessions replying to
each other automatically.

Judge repeats on the body rather than the id: a model in a retry loop
mints a fresh id every time, so the id guard cannot see it. Only for
messages from another session. A process this session started and a
controller the user minted a token for are exempt, because a hook that
reports two builds is reporting two facts and a person who says
"continue" twice means it twice; both are still rate limited.

A dropped message is never held, never delivered, and leaves no
tombstone, so a sender that waits out its burst and retries still lands.
The sender learns through a new `dropped` receipt carrying `dropReason`
and, when one receipt stands for several messages, `droppedMsgIds` — the
first drop is answered immediately and the rest are folded into one
receipt every few seconds, because a report about a flood must not scale
with it. The receiving user is told at most once a minute per sender,
with a count of what that line stands for.

A full input queue is now a drop naming the queue instead of an expiry,
which claimed a decision had run out when none was ever pending.

Sending sessions mirror the receiver's bucket per address and refuse a
send the receiver would drop, so the model is told to batch before the
message is written rather than a round trip later, and the receiver never
spends a connection on a message it was going to turn away. The mirror is
never stricter than the real limit, and a `rate-limited` receipt empties
it: the receiver is the authority on its own level.

No hop chain here — cutting a relay loop properly needs the path carried
on the frame and the current turn's inbound message known at send time,
which is separate plumbing. The buckets bound a loop to one message every
two seconds meanwhile, and adding the field later breaks no reader.

Part of #10925

* fix(ipc): keep peer drop reports truthful at the meter, mirror, and close (#11277)

- judge the repeat window on the larger of the wall and monotonic
  deltas, so a system suspend cannot turn an honest re-send into a
  duplicate
- roll the admission's body record back when an admitted message
  could not be delivered, so a retry after queue-full is not dropped
  as a repeat of a message that never arrived
- mirror the receiver's duplicate verdict in the send-side pacer
  instead of charging a token per send, and keep the burst count a
  refusal quotes this session's own
- defer an over-budget drop receipt to the next window rather than
  discarding it, so a legitimate sender under someone else's flood
  still learns its message died
- stop arrivals before draining the receipt coalescer at close and
  let the flush wait inside the cleanup budget, so receipts owed at
  shutdown reach their senders
- skip the socket-binding drop tests on Windows

* fix(ipc): keep the drop path honest about what it turned away, and about who

Round two of review on the admission work. Six behaviour defects, each
one a case where the reporting or the metering said something that was
not true.

**A full hold buffer no longer evicts.** It used to make room by
discarding its oldest parked message and telling that message's sender it
had `expired` — so an arrival destroyed a message the user had not read
yet, and an uninvolved third party was told a decision had run out that
nobody was ever making. A flood walked the backlog out one entry at a
time, which is the first thing this feature exists to stop. The newcomer
is turned away with `queue-full` instead, the shape the accept path
already had: the cost falls on the sender that could not fit, it is told
the truth, and it can retry. With no eviction left, `expired` means only
what the design doc says it means.

**The repeat record is rolled back wherever a message is settled unseen.**
A body is recorded when it is admitted, but admission is not arrival: the
gate can still refuse it, fail to queue it, park it into a full buffer, or
be shutting down. Left behind, the record turned the sender's honest retry
into a `duplicate` — a verdict whose whole premise is that the content is
already at the far side. Worse on the refusing path, where the sender's
"stop, nobody will see this" became "fold it into a later message". The
rollback restores what the admission displaced rather than clearing the
slot, so the body admitted before the failed one keeps its own protection,
and the token stays spent.

**The metering key no longer takes a peer at its word.** It is chosen by
what the transport established and the self-asserted address is namespaced
inside it. A peer could otherwise write `from: "own-process"` and share a
bucket, and a transcript line, with the scripts this session itself
started: it would spend their allowance, its flood would be announced to
the user as coming from their own process, and the receipts would be
addressed to a socket path that does not exist. The address is also
bounded now, since it is retained as a map key in three tables and nothing
that can be dialled comes near the cap.

**An evicted receipt batch no longer keeps a live timer.** It left the
table that `flush` and `dispose` iterate while still re-arming every few
seconds, so under a rotating `from` the orphans accumulated without bound,
each pinning a rejected message.

**A waiting batch holds an address, not a frame.** It kept the whole
untrusted `PeerUserFrame` for the trail, and longer while the receipt
budget was spent, so a flood the meter turned away lived on in the heap of
the session that turned it away. It keeps the three fields a receipt
actually addresses.

**A deferred receipt is bounded, and the close path forces one out.**
Deferring an over-budget receipt made its age unbounded, and a sender
turns a `rate-limited` receipt into a live throttle on itself — so a
minutes-late one paced an innocent sender against a wall that was gone.
Past that age it is abandoned. At the other end, `flush` used to dispatch
nothing at all when the budget was spent, which is exactly the sender the
deferral was added for.

Also: `close()` no longer serializes an unbounded leg ahead of bounded
ones, so the corrective receipts and the registry clear happen inside the
exit budget rather than after it; a refund can no longer un-drain the
mirror it raced, or erase a body record the receiver still holds; the
mirror refuses a repeat rather than writing a frame it knows will be
dropped; a drain corrects the level without re-anchoring the burst window
or inventing a mirror for a target nothing was sent to; and a drop notice
raised before the UI subscribes is replayed rather than swallowed after
spending its sender's minute.

The user-facing copy is corrected the same way: the trust category leads
the receiving line, `rate-limited` no longer accuses the named peer of a
rate it may not have set, the suppressed count is no longer called
"similar" when it is a session-wide total, a `duplicate` is no longer told
to re-send what was already accepted, the dedup window is read from the
constant rather than restated, and the expiry sentence keeps its retry
advice for the cases that are not a shutdown.

* fix(ipc): keep peer admission mirrors consistent

* fix(ipc): reconcile peer admission accounting

* fix(ipc): align peer pacing with session state

* fix(ipc): keep a late drop receipt useful, and bound the reply token where it is held

Five follow-ups on top of the round-5 fixes, all in the same PR's own
surface.

A `rate-limited` receipt that waited out a spent receipt budget was
abandoned on the receiving side once it aged past one bucket refill. That
traded a stale throttle for a worse silence: the ids it named stayed
`pending` on the sending side forever, and a sender told nothing cannot
tell a drop from a delivery that was ignored. The receipt now always
goes, and the part of it that can go stale — the throttle it implies — is
judged where the answer is known: the ledger stamps when each frame was
written, and a drop is decided the moment a frame arrives, so the age of
the send is the age of the drop. Past one refill the ids still settle and
the mirror is left alone.

`parsePeerFrame` refused a whole frame whose `replyToken` was longer than
the drop reporter retains. The message may be perfectly ordinary; the
token only routes the receipt. The bound stays where the token is
retained, and the frame is delivered.

`DropReceiptCoalescer.note` accepted its arguments two ways for a caller
that does not exist — the method is new in this change.

The global burst's own reasoning was in neither document: it is half the
64-send outbound ceiling because every admitted message draws a receipt
against that ceiling. Both docs say so now, the module header no longer
claims the per-sender bucket is the binding one, and a test pins the
relationship so a retune of either number cannot quietly break it.

Three guards had no witness: the meter reset when a session id changes,
and the repeat-record rollback at hold expiry and at a policy flip. Each
new test fails when its guard is removed.

* test(cli): give the settled-receipt stubs the age the ledger now reports

`SettledPeerReceipt` carries `ageMs` so the drop path can tell a live
rate limit from one the receiver has already refilled past. The injected
stubs in the messaging tests still returned the old shape, which
type-checks against a stale core build and fails against a fresh one —
so the package build, which is what CI runs during install, was the
first thing to see it.

---------

Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-09-09 08:06:01 +00:00
Shaojin Wen
9163bdcddb
feat(serve): one-command remote start with a generated token, same-origin shell access, and a pairing QR (#11172)
* feat(serve): make non-loopback binds usable with one command

Binding 0.0.0.0 used to require coordinating four things by hand: an
operator-generated token, the exact reachable browser origin in
--allow-origin, a hand-built #token= URL for the browser, and patience
for a shell that 403ed every mutation without the allowlist entry.

- A non-loopback bind with neither --token nor QWEN_SERVER_TOKEN now
  generates an ephemeral 256-bit bearer instead of refusing to boot,
  printed once at startup with the concrete local/private-LAN URLs and
  a secret-bearing QR for phone access. Explicit sources keep
  precedence; explicitly empty sources still fail closed; a localhost
  request resolving off-loopback never generates. The QR re-publishes a
  stable operator token only at an interactive TTY, never into captured
  stdout. Advertised addresses reuse Local Control's private-LAN
  population, so VPN/docker/public addresses are never printed or
  encoded. The block writes through the non-throwing stdout helper so
  a vanished pipe reader cannot kill the listening daemon.
- The built-in Web Shell's same-origin HTTP requests pass the origin
  wall on a remote bind once they carry a verified bearer; forwarded
  headers never establish same-origin trust, cross-origin and null
  origins stay denied, and Local Control listeners are excluded.
  WebSocket routes (terminal, voice) and TLS-front-proxy origins
  deliberately keep the allowlist requirement, named by a narrowed
  startup warning and the docs.
- The standalone browser bootstrap gains a credential gate: token form
  on 401, distinct origin-policy message on 403, auto re-probe honoring
  Retry-After during cold start, permanent startup failure surfaced
  instead of polled, 10s-bounded abortable probes, bilingual en/zh-CN
  copy, per-tab token storage, and URL scrubbing in every build mode.
- The pre-auth shell discriminators move to a dependency-light module
  so the serve fast-path static closure keeps deferring the
  express-static/CSP machinery; the import-boundary guard now covers
  the transitive edge.

Docs: user guide remote quickstart + authentication/options/security
sections, developer daemon boot-refusal references, CLI help, and a
design doc recording the security decisions and three deliberate
deferrals. Tests: token resolution/precedence/rotation, LAN filtering
and QR gating, same-origin middleware incl. production mount position,
bootstrap-gate retry/timeout/auto-recovery, DEV scrubbing, env-guard
and fast-path guards.

* feat(serve): shorten the generated remote bearer to 128-bit base64url

A 64-hex credential was painful to type into the Web Shell gate and made
the pairing QR denser than it needs to be. The generated bearer is only
ever attacked online (it is never stored, rotates per process, and the
daemon answers 401 without side effects), so 128 bits of CSPRNG entropy
in 22 URL-safe base64url characters keeps the brute-force budget
astronomical while being typeable and scannable. Operator-supplied
tokens and the loopback --open-with-auth credential are unchanged.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* feat(web-shell): present the daemon credential gate as a device-authorization card

The pre-mount gate rendered as bare unstyled HTML. Compose it from the
shared Card/Button/Input/Label primitives with semantic theme tokens:
centered card, daemon address as mono description, code-style token
field, full-width primary action, and a bilingual security hint — the
same visual grammar as familiar device-authorization pages.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(web-shell): theme and scope the credential gate like the app root

The gate rendered before App mounts, outside the scoped Tailwind/shadcn
scope and without the theme palette, so it fell back to unstyled light
markup. Opt into the same scope attributes, theme module class, and dark
class the app root applies, and pass the resolved initial theme from the
boot entry.

* fix(serve): address PR review — sweep boot-contract docs, harden quickstart edges, refine gate retry semantics

Review responses for #11172:

- Docs: sweep every standing statement of the boot contract (user guide
  bullet and flag rows, developer auth/quickstart/runtime references) to
  the new contract — generation supplies the remote token, same-origin
  HTTP needs no --allow-origin while WS upgrades and TLS front proxies
  still do; scope the LAN-advertisement guarantee to wildcard binds and
  document the container/pty/name-heuristic caveats.
- Daemon: reject an empty --hostname as operator error before credential
  generation; treat inet_aton wildcard abbreviations as wildcards; prefer
  a routable private address for the QR; make the QR fallback line
  state-independent; suppress the quickstart when the socket actually
  binds loopback; move access-log and trace capture ahead of the origin
  wall so their rejects are recorded; normalize Host (case, default
  ports) in the same-origin check.
- Web Shell gate: key permanent startup failure on body.code, parse
  HTTP-date Retry-After, show not-ready copy for 429, clear a rejected
  stored credential instead of pre-filling it, tag the gate root with
  data-web-shell-gate so e2e readiness locators no longer resolve on it.
- Tests: call-site spy for the quickstart arguments, wildcard/QR-candidate
  fixtures, Host normalization and listener-exclusion cases, gate retry
  shapes, storage-blocked cache pin, main-boot mock completeness, and the
  fast-path guard transitive edge.

* docs(serve): scope the remaining allow-origin refusal statements to loopback binds

The --allow-origin flag row and the auth reference's allowOriginCors
bullets still stated the '*' / non-loopback-origin boot refusals
unconditionally; on non-loopback binds the generated ephemeral token
satisfies those guards, so the refusals are loopback-only. Completes the
R1-3 contract sweep from the PR review.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(serve): keep the loopback Host gate ahead of pre-auth health

The access-log/trace move for the origin wall initially left hostAllowlist
below the pre-auth health routes, dropping the DNS-rebinding defense from
/health on loopback binds. Final chain: access-log, trace-id capture,
hostAllowlist, same-origin strip + credential check, allowOriginCors,
pre-auth health. Pre-auth health therefore sits below the origin wall
(matched cross-origin probes keep their CORS headers) and below the
access log, so liveness probes are logged — the unavoidable price of
logging the origin wall's rejects. Adds a regression test pinning the
Host gate ahead of pre-auth health and syncs the auth reference diagram.

* fix(serve): address round-2/3 review — bound-address print modes, gate pins, doc contracts

Quickstart now classifies by the address the socket actually bound
(loopback binds print token-only or stay silent), normalises the
IPv4-mapped wildcard, and skips software-interface ULAs; the boot gate
passes the bound address and a broken-pipe guard keeps a vanished pipe
reader from taking the listening daemon down. Web Shell gate clamps
Retry-After at 30s; e2e barriers exclude the gate root; self-origin,
access-log mount position, and fast-path graph pins replace regex
enumeration. Bootstrap middleware order matches the runtime chain and
the daemon doc set is swept to the same contracts.

* docs(serve): name the Local Control Host gate in the chain diagram label

* test(web-shell): pin the Retry-After floor and garbage-value edges

* test(serve): pin the explicit-bind spelling against the bound address in full mode

* docs(serve): keep the open-with-auth credential sentence from reading as tokenless non-loopback

* fix(serve): address round-4 review — spelling-keyed generation carve-outs, dead-branch cleanup, missing pins

Docs now name the localhost-resolves-off-loopback carve-out everywhere
generation is described, the protocol reference states the same-origin
exception ahead of the CORS wall, and the empty-hostname dual-stack claim
is gone. Code drops the provably dead boundFamily branch and OR operand,
scopes the proxy remedy to the plain-HTTP case, resets the gate live
region at probe start, and updates the embedded-caller token JSDoc.
New pins: resolved-token boot guards, bootstrap same-origin window,
inet_aton bound address, web flag, EPIPE guard, boundAddress
normalisation, TLS print path, no-candidate fallback, re-parse guard,
GET/HEAD pre-auth gate, and the gate hint/status copy.

* fix(serve): address round-5 review — blank-source and fragment-delivery contracts, pin premises

Docs now state that any supplied-but-blank token source refuses without
generating (not just the localhost carve-out), that --open fragment
delivery keys on the resolved token regardless of flag, and that the
same-origin exception authenticates only on the primary listener with
the pre-auth shell routes exempt. The intermediary criterion is Host
rewriting, not port translation, across the four doc lines and the boot
stderr; webhook ingress joins the pre-auth enumeration; the --token
"required when" cell lists all three loopback conditions; the embedded
JSDoc names QWEN_DAEMON_TOKEN as the worker delivery channel. New pins:
remote-HTTP(S) allow-origin boot arm plus loopback negative, plaintext
warning positive assertion, non-canonical/unparseable/hostless/crossed
self-origin rows, bootstrap premise assertions, stderr EPIPE arm, and a
dedicated install-order file for the broken-pipe guard.

* docs(serve): scope port-translation to non-loopback and narrow blank-token shadowing

Port translation alone needs nothing only on a non-loopback bind; on the
default loopback bind the DNS-rebinding Host allowlist rejects a
translated port for every request including the shell document, and no
--allow-origin overrides it. Blank-source shadowing is one-directional:
a blank --token shadows a set env, but a blank env never shadows a
non-blank --token, while blankness suppresses generation either way.
The security reference's Overview bind invariant and quick-reference
hostname row now state that a non-loopback bind always carries a
bearer, with the two documented refusals named.

* fix(web-shell): reset the gate only on operator-initiated probes

The status-reset fix for the stale live region fired on every automatic
retry too, flipping the submit button and re-announcing two strings once
per cold-start cycle. An operator-probe ref now marks manual submits and
the initial mount; automatic retries leave the transient message and an
enabled button in place, so a manual retry can genuinely jump the queue.
The suite also gains the documented contract rows: HTTP-date ceiling
clamp, body.code-beats-Retry-After precedence, no auto-retry on 401/403,
destination baseUrl and enterToken copy in both languages, typed-submit
trimming, theme palette classes, and manual-supersedes-armed-retry.

* fix(serve): settle the round-7 deferred list — scoping claims, boot pins, refusal message

The non-loopback bind refusal now says an explicitly blank --token or
QWEN_SERVER_TOKEN counts as no token, so both surviving causes get
advice they have not already followed. The access-log exemption is
documented as exact-path (GET /health and POST */heartbeat), with
HEAD /health and GET /health/ named as logged, in the server comment
and both doc tables. #token= delivery is described as keyed on a
resolved token; "loopback binds never generate" becomes "loopback
spellings" in all three spots; the --token "Required when" cell names
the blank-value refusal; the pre-auth exception enumerations add
loopback /health. New pins: the bootstrap Host gate ahead of the CORS
wall via a raw-socket rebinding probe, the wildcard-bind premise read
from server.address() instead of the echoed option, and tmpDir
workspaces for the wildcard boot tests so the checkout is not
registered.

* fix(serve): charge pre-auth gate rejects to their own access-log budget

The walls moved above the access log to make their rejects auditable, but
that put the operator's own request lines behind a credential-less
flood: ~30 s of >2 req/s rejects drained the shared 60/2-s burst and
held authenticated traffic suppressed behind the aggregate warning.
Host-allowlist, CORS-wall, and remote same-origin credential rejects now
stamp a res.locals marker and draw from a separate 30/1-s budget, so a
reject flood coalesces loudly while operator lines keep their own
budget; the marker clears when a same-origin bearer verifies. Pinned at
the middleware level (65 marked rejects leave the operator line logged,
30 individually logged, aggregate warning still fires) and at the app
level (a 65-request wall flood cannot starve a subsequent authenticated
request's line).

* fix(serve): tolerate shim responses without locals when stamping the reject marker

* fix(serve): isolate the pre-auth reject accumulator and stamp the null-origin arm

The shared suppressed accumulator's flush spent an operator token per
marked reject once the reject budget drained, moving the starvation
boundary from ~2 rejects/s to ~3-4/s instead of eliminating it. Each
budget class now owns its accumulator, flushes from its own tokens, and
the shutdown seal drains both; the null-origin CORS arm joins the
stamped reject set. Docs: the bootstrap paragraph names the delegating
wrapper (only the three bootstrap paths go unlogged), the third
port-translation spot gets the bind qualifier, the bearer 401 budget is
named as unchanged, the localhost carve-outs note that any resolved
token source satisfies the refusal, generation is described as
spelling-keyed everywhere, and the empty-value clause names the loopback
trusted-mode outcome.

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-09 07:16:30 +00:00
jinye
ada633d584
fix(external-context): Align deletion responses with Mem0 SDK (#11397)
* fix(external-context): Align deletion responses with Mem0 SDK

Accept successful HTTP responses and parse JSON without fixed acknowledgement rules, retaining exact post-delete absence verification.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(external-context): Clarify deletion confirmation and cover success statuses

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-09-09 07:09:28 +00:00
易良
b492ba8b41
docs(serve): state what --no-web actually removes (#11446)
The API-only docs claimed --no-web does not narrow the REST/SSE API. It does: POST /workspace/local-control/enable is registered unconditionally but receives webShellAvailable=false, so it fails closed with 409 local_control_web_shell_unavailable on every platform, and on macOS liveVoiceSurfaceAvailable requires serveWebShell !== false, so the /live/* routes, the /live/host WebSocket and the experimental.liveVoice.* settings keys are never registered. Say what the flag removes instead of denying it. Also point the daemon index at the embedding boundary that replaced page 20's deleted invocation recipes.

Follows up the review comments on #11314, which merged before they were posted.
2026-09-09 07:01:52 +00:00
易良
d8c505bf42
docs(nav): stop hiding the developer examples section (#11426)
`examples/daemon-client-quickstart.md` is the only end-to-end walkthrough of
the daemon HTTP API, and 59163a3 just rewrote it into the API-only form an
external REST integrator wants. `display: 'hidden'` keeps it out of the nav on
both sites, so the page is reachable only by guessing its URL — which is what
#11359 had to do, linking it as a GitHub blob.

Its sibling `proxy-script.md` is in the same position.

Pairs with QwenLM/qwen-code-docs#263. The published site carries its own copy
of this file (the docs pipeline never syncs `_meta.*`, see #11399), so both
have to change: that PR makes it visible now, this one keeps the two copies
agreeing so the next manual mirror does not re-hide it.

Refs #11359, #11399
2026-09-09 06:58:55 +00:00
ytahdn
7bf0cafab4
feat(web-shell): separate Plan workflow from execution permissions (#11423)
Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
2026-09-09 06:39:57 +00:00
jinye
dd4cd4a08b
refactor(serve): decouple workspace capacity policies (#11428)
* docs: record workspace capacity measurements and rollout plan

Record the controlled capacity baseline and separate registration expansion
from optional runtime dormancy and LRU work. Document the P0 policy owners
and their unchanged compatibility boundaries.

Refs #11386

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* refactor(serve): decouple workspace capacity policies

Give registration admission, modeled child capacity, and channel-control
timeouts independent defaults while preserving the public legacy export.
Cover the removed coupling and the SDK timeout override boundaries.

Refs #11386

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-09 06:25:35 +00:00
BaboBen
cf987cdc59
feat(dingtalk): present tool permission requests with native interactive cards (#10457)
* to #10388 [feat] Present DingTalk permission requests as cards

Co-authored-by: Codex Using gpt-5.6-sol

* to #10388 [fix] Localize DingTalk permission cards

* fix(channels): handle permission response races

* fix: preserve zh always-allow scope suffix and stabilize gate test legs (#10457)

- ChannelBase: translate the stock prefix of scoped always-allow labels
  but keep the command/tool scope suffix (exec/mcp confirmations always
  generate suffixed names, and zh channels dropped them)
- packages/cli: slow-host test budgets now also key on measured host
  saturation (isSlowTestHost), because the review-address verification
  gate re-runs tests through an env -i child that does not carry
  RUNNER_NAME; raise the dev-entrypoint startup budget accordingly
- packages/cli: llm/AppContainer/live-serve tests point HOME at a
  scratch directory; the gate inherits a HOME the test process cannot
  write to, which crashed best-effort ~/.qwen writes

Adds mutation-witnessed tests for permission-card controller guards
(question_id, checkbox payloads, hasBusinessPayload gate, isCancel,
requestId action, per-actor forbidden dedupe) and the zh user-scope
always-allow branch.

* revert(cli): restore RUNNER_NAME-keyed vitest config (#10457)

The verification gate rejected the previous commit because
packages/cli/vitest.config.ts is test-config machinery this PR never
touched. Restore that file exactly to its pre-round state; the rest of
the rejected commit (the zh always-allow scope-suffix fix and the
per-file test accommodations) is preserved.

The environmental gate-leg blocker the reverted change tried to work
around — the gate's env -i clean child stripping RUNNER_NAME and
QWEN_HOME while passing through a HOME the tests cannot write to — is
escalated to the maintainer in the round summary instead.

* fix(channels): attach the expiry denial handler at creation (#10457)

A timed-out permission card fired respond('deny') and then suspended on
the terminal card projection, leaving the denial promise without a
handler for the whole window. When the bridge rejected inside it, the
rejection escaped as a process-level unhandledRejection before the
trailing catch ever attached, and Node logged a second
rejectionHandled warning once the late await picked it up.

Attach the handler where the promise is created so the expiry denial is
reported as soon as it fails. The claimed-state assignment stays ahead
of the call, and respond still precedes the projection.

* test(cli): stop deleting the AppContainer scratch HOME under in-flight work (#10457)

AppContainer's mount effect runs the real config.initialize() in an
un-awaited IIFE, so extension-store lock work against this suite's
scratch HOME can still be queued when afterAll runs. Deleting the tree
there fails that work with ENOENT, which withLock rethrows as
ExtensionStoreBusyError into a promise nobody awaits: all 171 tests pass
and the packages/cli leg still exits non-zero on one unhandled rejection.

Leave the tree for the OS to reclaim. Deferring the deletion to a process
'exit' handler was measured first and is not an option here — vitest
workers in this repo never run it.

* style(core): format Anthropic SSE retry test

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-09-09 05:50:50 +00:00
jinye
a7a6addd4e
feat(web-shell): add opt-in browser task notifications (#11398)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-09 05:46:06 +00:00
probe
eded8817a7 fix(web-shell): recover from initial workspace discovery failures 2026-09-09 13:41:34 +08:00
qqqys
cfb173ec46
refactor(channels): remove obsolete block streaming (#11381)
* refactor(channels): remove obsolete block streaming

* docs(channels): explain the seams the block-streaming removal left behind

Follow-up review cleanup on top of the block-streaming removal. No behavior
change — comments and one test suite name only.

- DingTalk's deliverBackgroundReply override is now body-identical to the base
  seam, so it reads as redundant. It isn't: the background aggregation flush
  passes prepared/failOnHttpError, which the base signature does not declare.
  Say so at the override.
- The Feishu "no card session" branch comment lost its blockStreaming half and
  was left mid-sentence. Name the gate that actually reaches it.
- The renamed block-streaming suite collided with the existing 'response
  delivery' describe, so the file had two identically named suites and a -t
  filter matched both. Give the second one a distinct name.

* test(channels): preserve delivery regressions

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-09-09 04:41:31 +00:00
qqqys
7eeeccea29
fix(cli): cap the background-notification queue and tell the model what was dropped (#11351)
* fix(cli): cap the background-notification queue and tell the model what was dropped

The unified notification queue in the interactive TUI was an unbounded
array: all six producers pushed without checking its length, and it was
only ever cleared on a session switch. A noisy producer could therefore
accumulate an arbitrary backlog that a single idle edge then fed into one
model turn. The interim-monitor cooldown added in #10848 limits how often
pulses start a turn, but pulses still pile up unbounded inside the
cooldown window.

Extract the admission rule the ACP Session already had into
`background-notification-queue.ts` so both front ends share one cap (20)
and one eviction order, and record every loss instead of writing it only
to a debug log.

Eviction prefers the oldest interim monitor pulse — the monitor's next
poll supersedes it — and otherwise takes the oldest unprotected entry.
The TUI protects agent results, workflow results and cron prompts, none
of which have a second copy anywhere; when every queued entry is
protected the incoming notification is dropped instead. ACP keeps its
existing protection rule (entries continuing the todo-stop-guard work
chain) and its existing order: it filters interim pulses before they are
queued, so pulse priority is inert there.

Losses are reported once, on the next drained turn, as a single summary
line naming how many notifications went and which tasks they came from —
per-loss lines would reproduce the flooding the cap exists to stop. The
summary is parked if that turn is rejected, so a failed admission cannot
lose the only record of what was discarded.

* fix(cli): let the admission rule read the arriving notification

Review follow-up on the review of 866163005a.

`decideNotificationAdmission` never read its `incoming` parameter, which
told the next reader the arriving item mattered to the decision when it
did not. Neither tsc nor ESLint flags it — `noUnusedParameters` is off
and the rule's default `args: after-used` ignores a parameter followed by
a used one — so the signature would have shipped misleading.

Give it the meaning it should have had: when the queue holds no interim
pulse to evict and the arriving notification is itself a pulse, drop the
newcomer rather than displace a terminal result. A pulse is superseded by
the monitor's next poll, so evicting the only copy of a shell result to
make room for one trades the wrong way — the same reasoning that already
puts queued pulses first in line. ACP is unaffected: it filters pulses
before they are ever queued, so an arriving item there is never interim.

Also from the review:

- Project the ACP queue for the admission decision, not just for the
  tally. `interim` exists only on the projection, so passing raw entries
  would have silently disabled pulse priority for whoever relaxes the ACP
  monitor filter, which is exactly what the projection's docstring
  promised would keep working.
- Reattach the `QueuedNotification` docstring, which had been left
  stranded above `PendingDroppedSummary` when the two interfaces were
  reordered.
- Record at the call site why the ACP does not park the summary across a
  refused turn the way the TUI does: past admission the summary shares
  the notification's fate, and that path does not re-queue the item
  either.
- Stop the docs claiming "nothing is lost silently". The drain prunes
  pulses from cancelled monitors and can empty the queue before the
  summary is taken, so the summary rides along with whatever notification
  drains next and is discarded unreported if the session is cleared
  first.

* fix(cli): preserve notification overflow semantics

* fix(cli): preserve notification overflow reporting

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
2026-09-09 04:13:34 +00:00
qqqys
eb383fd3bb
feat(core): enable the built-in web_search by default on ModelStudio Standard/Token Plan (#11348)
* feat(core): enable the built-in web_search by default on ModelStudio Standard/Token Plan

web_search was opt-in and needed enabled=true, a search model selector, and a modelProviders entry with an envKey even though a ModelStudio user's primary entry already satisfies every condition. Declare a dashscope search backend on the Standard API Key and Token Plan presets and let the registry derive the search side channel from the primary model's entry when nothing is configured explicitly. Entries matching no preset are adopted when their host is a DashScope endpoint, covering hand-written entries and the workspace-specific Token Plan hosts; Coding Plan endpoints stay out until they are verified.

Explicit model/env configuration keeps working for any DashScope-compatible entry. Auto derivation fails silently so users on other providers see no notice, enabled=false (or ENABLE_WEB_SEARCH=false) turns the tool off, and bare/safe mode always does.

* fix(core): derive the env-only web_search backend from the resolved generation config

The auto path read the runtime model snapshot, which is empty when the tool registry is built — detectAndCaptureRuntimeModel() runs after createToolRegistry() — and which only carries apiKeyEnvKey when a modelProviders entry supplied it, so a pure OPENAI_BASE_URL / OPENAI_API_KEY setup never registered web_search. Read the resolved generation config instead, which the ModelsConfig constructor already has, and fall back to the auth type's default key variable when no entry named one.

Also correct the docs: the default Auto approval mode lets the classifier approve searches without prompting, and a Custom Provider entry on a DashScope host is adopted like a hand-written one.

* fix(core): keep a failed web_search backend derivation from breaking the tool registry

Reading the generation config to derive the search backend assumed a complete Config surface, so a test double that stubs getModelsConfig() without getGenerationConfig() threw while the registry was being built and took every other tool down with it. Derivation is opportunistic: an unexpected Config shape should cost web search alone, so catch and fall back to the silent no-backend result. Give the subagent-manager double the accessor it was missing.

* chore: re-trigger CI

The Qwen Code CI workflow was not scheduled for 0149317b95 (every other workflow on that commit ran); this empty commit re-runs it.

* fix(core): address web search review findings

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
2026-09-09 04:08:29 +00:00
Shaojin Wen
88b0afaeb1
docs: require bilingual design documents (#11419)
Co-authored-by: probe <probe@local>
2026-09-09 03:41:56 +00:00
qqqys
c069801657
feat(workflows): journal failed agents and settle agent failures to null (#11196)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Lint & Static (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / web-shell E2E Smoke (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (no-AK, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (CLI, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Desktop Shell (ubuntu-22.04) (push) Blocked by required conditions
Qwen Code CI / Desktop Shell (windows-2022) (push) Blocked by required conditions
E2E Tests / Build for E2E (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 1/2 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 2/2 (push) Blocked by required conditions
E2E Tests / E2E Interactive - OpenTUI renderer (bun) (push) Blocked by required conditions
E2E Tests / channel-plugin E2E (nightly) (push) Blocked by required conditions
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Blocked by required conditions
* feat(workflows): journal failed agents and settle agent failures to null

A workflow's journal recorded that an agent started and that it returned,
and nothing in between. A key with a `started` and no `result` could be an
agent that failed or a run that was interrupted with that agent in flight,
and a resume had no way to tell the two apart — it re-ran both and said
nothing about either.

The same gap ran through the script contract. A `parallel()` slot whose
agent hit its turn cap became `null`, while the identical failure in a bare
`await agent()` threw and ended the run, so the shape of the call decided
whether one broken agent cost a slot or the whole workflow.

Agent-level failures — turn cap, time cap, model error, no structured
result, the user stopping that agent — now settle to `null` in both forms
and are journaled as `failed`. Run-level conditions still throw: the token
budget, the agent cap, a stall that survived every attempt, a cancelled run.
A cancelled run writes no `failed` records at all, so its open keys read as
interrupted rather than broken.

What failed is now reported rather than counted: the run trailer and the
background completion notification list each failed agent with its error,
capped with a named remainder, and a resume says which calls it is re-running
and why. The run event carries agents_failed, agents_cached and
agents_respawned.

Part of #11013

* fix(workflows): only report a respawn for a call that never finished

The respawn report fired for every call that ran live on a resume, not
only for the ones the journal had left unfinished. Once any call misses,
the prefix invariant sends every later call live too — including calls
that completed last time — so an ordinary interrupted fan-out reported
its already-finished agents as interrupted alongside the one that really
was, and counted them.

A key with a journaled result is now excluded: it is re-running because
of what happened upstream of it, which is a fact about the run rather
than about that agent.

Part of #11013

* fix(workflows): align failure settlement and resume reporting

* fix(workflow): preserve run failure semantics

* fix(workflows): journal starts only after dispatch admission

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-09-09 01:17:45 +00:00
probe
bdfa63613a fix(web-shell): expose known capabilities on initial connection 2026-09-09 09:05:24 +08:00
Shaojin Wen
a5adf4f91c
Merge branch 'main' into codex/fix-web-shell-cold-session-loading 2026-09-09 07:00:54 +08:00
probe
0bfc1bbd3b fix(web-shell): avoid duplicate cold session restoration 2026-09-09 06:52:41 +08:00
qqqys
1f890086f1
feat(goal): carry budget figures and progress guidance in the continuation prompt (#11257)
* feat(goal): carry budget figures and progress guidance in the continuation prompt

The continuation prompt told the model what the objective was and how to
deliver it, and nothing about two things it had no other cheap way to know.

It could not see how much of the spend window was left. A Goal stops when
`tokensUsed` reaches `tokenBudget`, 30,000,000 by default, and gets one
wind-down turn to hand off; until that turn arrives there was no signal at
all, so the model could not tell turn 3 of a long run from the turn before the
budget stops it, and could not choose between opening a broad investigation
and finishing what it had. `get_goal` does not carry the figures either, so
there was not even an expensive way to ask.

It was also never asked whether its last turn accomplished anything. The
verifier only ever sees a terminal proposal, so a turn that proposes nothing is
judged by nobody -- and a turn spent restating status is exactly the turn that
proposes nothing.

Each runtime-scheduled turn now opens with what has been spent, out of what,
what remains, and how many turns are behind it, followed by four standing
lines: treat the workspace rather than the conversation as authoritative; work
toward the end state the objective asks for rather than a more easily reached
one; judge whether the previous turn actually changed anything before spending
this one; and check every requirement against citable evidence before
proposing completion.

The figures sit after the standing objective guard and before the
objective-updated notice, which is about what changed since the last turn and
so reads last. They stay out of the data block on purpose: that block is
compared by content to decide whether the objective changed, and a number that
moves every turn would make every turn look like an edit. The remainder is
clamped at zero, since the hand-off turn runs with the window already
overspent.

The progress lines are skipped on that hand-off turn, which is told not to
start new work -- a line asking for "a different concrete action now" would
contradict it. The budget line stays, because a hand-off reports the numbers it
stopped at.

`usage` is optional on the host contract, so a host with no figures renders
exactly the prompt it did before. The three hosts copy it through their queue
entries alongside the fields they already copy.

* test(goal): pin the budget figures through each host, and skip the judgement on turn one

The three host copies were the only link in the chain with nothing behind
them: `usage` is optional on both sides of every hop, so deleting a copy
typechecks and costs the prompt its budget line on that host alone. One
case per host now fails when its copy is removed.

The judge-your-previous-turn line is held back on the Goal's first turn.
`create` schedules a continuation before any turn has finished, so that
line asks the model to judge a turn that does not exist. A host that
reports no figures still gets the line: not knowing the turn number is
not evidence of a first turn.

* fix(goal): satisfy formatting checks

* fix(goal): address continuation prompt review

* refactor(goal): centralize continuation payload

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-09-08 19:11:46 +00:00
Shaojin Wen
70cf363395
feat(web-shell): Improve split-view session navigation (#11250)
* feat(web-shell): Improve split-view session navigation

* fix(ci): Run Web Shell browser checks on hosted runners

* fix(web-shell): Keep split navigation outside approval shortcuts

* fix(web-shell): Address split-view review regressions

* fix(web-shell): Stabilize split approval reports

* fix(web-shell): Preserve history anchors during slow rendering

---------

Co-authored-by: 易良 <1204183885@qq.com>
2026-09-08 17:33:19 +00:00
易良
562ea5a0f0
fix(docs): stop daemon capability totals drifting (#11370)
* docs(serve): sync the daemon capability tag count with the registry

Two capability tags landed on main in succession and only one of them bumped
the documented count. The worktree-reset tag came with the index update, taking
the documented total to 158; the extension-activation refresh tag arrived
afterwards and left the document alone, so the registry now holds 159. Each PR
was green against its own base — the second one branched before the first
landed — and the contract test only sees the mismatch once both are on main.

The count is asserted against the registry rather than hand-maintained, so this
is a one-character correction with the test as its own verification: red before,
green after.

* fix(docs): remove volatile capability totals

---------

Co-authored-by: Shaojin Wen <szujobs@gmail.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-09-08 11:54:31 +00:00
易良
6b7e5615c7
fix(web-shell): show session active work (#11267)
* fix(serve): report child-owned session turns as active work

* fix(web-shell): show session active work

* fix(web-shell): gate todo spinner on live work

* chore(web-shell): format sidebar after merge

* chore(web-shell): use repository formatter

* fix(acp): retain active work during session registration

* fix(web-shell): clarify active work archive warning

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-08 11:54:21 +00:00
ChiGao
7023bba760
feat(cli): OpenTUI parity closeout (dialogs, composer, shell mode) (#11152)
* fix(cli): render the OpenTUI confirmation as one surface and bound the transcript flood

The mem0 confirmation scenario exposed three stacked gaps in the OpenTUI
renderer: the awaiting-approval path never reached the transcript model,
the expanded dialog body overflowed a fixed alt-screen viewport, and the
confirming card duplicated the payload the dialog already carries.

Emit confirm/confirm-resolved transcript events from the real scheduler
awaiting path, render the expanded body as a tail window budgeted against
the terminal height (alt-screen has no scrollback to fall back to), let
the pending card yield its description to the dialog, and cap every tool
card description to a bounded head with ink's own hidden-tail vocabulary.
A shared config.initialize() once-guard stops submit-time "Chat not
initialized" races, and a batch where every call was cancelled now ends
the turn without a follow-up model request, matching the ink stream.

The mem0 e2e post-approval wait moves to request bodies so it does not
depend on raw pty bytes; both legs pass all five scenarios.

* fix(cli): auto-open the OpenTUI auth dialog at boot (U-6)

Ink's boot auto-open is two triggers: no auth type configured (useAuth
initial state) and the one-shot startup authError
(useInitializationAuthError); shouldOpenAuthDialog is dead in production
code. The OpenTUI entry now computes that request once and seeds the
shell's dialog state with it, and the auth dialog surfaces the startup
failure through its existing error surface. Also repairs the entry
fallback-contract tests, which had been failing since f6213a18cd because
their mock config lacked initialize for the shared once-guard.

* fix(cli): drain deferred OpenTUI update notifications on idle (G-3)

Ink flushes handleAutoUpdate's pendingNotifications when the turn returns
to idle; the OpenTUI entry only kept cleanup, so a mid-turn update notice
sat queued forever. Hold { cleanup, flush } in a ref and flush when
live.streaming flips false, mirroring AppContainer's idle effect. The
check bootstrap, banner slot, and registration were already renderer-
neutral on main — the ledger's G-3 gap was the drain only (Decision 9).

* fix(cli): route OpenTUI settings sub-dialogs and own the composer fill (U-9)

The mount's fillInput/onSelectSetting seams existed since Batch 4 but the
shell fed neither, so settings sub-dialog rows reported an unmounted
dialog and the arena picker's fill was reported lost. The shell now
routes ui.theme/editor and the fast/vision model rows like ink's
DialogManager, and fills the composer through the entry-owned handle via
the polling injector (the picker detaches the handle before remounting
the prompt). With an owner present a named selection no longer closes
afterwards — the owner replaces the request, and closing would clobber
the dialog it opens (Decision 10).

* feat(cli): OpenTUI follow-up suggestions with the ghost placeholder (U-7/G-2)

Port the ink follow-up suggestion feature: the entry generates on the
streaming→idle edge through a new useFollowupSuggestionGeneration hook
(ink's gate set verbatim; abort on every turn boundary and unmount), and
the composer consumes it via the shared renderer-neutral controller —
ghost placeholder, Tab/Right/Enter accept into the buffer without
submitting, typing dismisses, submit clears.

* fix(cli): correct WaitingCallInfo import source in follow-up generation

WaitingCallInfo lives in live-session.ts, not live-session-model.ts; the
wrong source only surfaced under tsc because the hook never calls the
type at runtime. Also give the parked-confirmation test case a
well-formed WaitingCallInfo and pin the it.each table to
Partial<FollowupGenerationParams> so the per-case unions typecheck.

* feat(cli): record OpenTUI steering hops for /resume (U-32)

Ink's steering accept() writes each surviving steered message through
recordMidTurnUserMessage so a session resume replays the mid-turn API
shape (user parts riding the same Content as tool results); the OpenTUI
port carried the user echo but never recorded, leaving steers invisible
to /resume. resolveSteeredPromptParts now collects per-message
recordings alongside the echo events — same membership as the echo, so
a declined message is neither echoed nor recorded — and the drain loop
records them at the commit point, after the restore check: a restored
(aborted) hop records nothing, keeping the all-or-nothing contract.

* fix(cli): join the OpenTUI queue Esc-restore with a blank line (U-11)

ink aggregates popped queue texts with '\n\n' everywhere
(aggregateUserMessages, getQueuedMessagesText), matching the model-side
steering join; the OpenTUI popQueue used a single newline. Align to the
blank line. ink's peer and slash-command queue filters have no OpenTUI
counterpart to port: the live queue only ever holds plain composer text
(slash commands defer in the shell instead of queueing) and peer
messaging has no OpenTUI queue.

* feat(cli): render the four command cards in the OpenTUI transcript (U-34)

advisor, away_recap, arena_agent_complete and arena_session_complete
previously projected to null, so /advisor, /arena and /recap output was
invisible on OpenTUI. Each kind now carries a structured stream event
through the fold into a dedicated transcript row mirroring the ink
component shapes (recap gutter with bold label and italic body, advisor
markdown card, arena status/token/tool lines and comparison sections).
Status colors map onto the live palette so rows track theme swaps; the
arena comparison helpers are mirrored from the ink card. tool_use_summary
and diff_stats stay explicit no-ops (no OpenTUI writer for either), and
the kinds remain live-only: neither renderer replays them on resume.

* feat(cli): add the OpenTUI ! shell mode (U-33)

Empty-buffer `!` toggles shell mode (Esc exits), the chrome shows the `!`
prefix, slash dispatch stays first, and a submission runs through a thin
executor over core's ShellExecutionService — a `$ ` command row plus a
synthetic run_shell_command card on the transcript, with the command+result
injected into the LLM history via ink's shared processor.

* fix(cli): close Batch 10 review-round findings on the OpenTUI surface

- Reconciler crash: stop nesting <text> inside <text> in the theme,
  permissions, settings, and MCP dialogs (five sites, one mechanism).
- Shell mode: derive the card id from a UUID so two commands started in
  the same millisecond cannot collide, and report generic tool-end
  summaries so the `!` card no longer prints the command twice.
- Confirmation body: ignore ctrl-s on a body that already fits — the
  expanded tail window can be shorter and would silently drop head rows.
- Transcript replay: pass mid_turn_user_message through to /resume while
  side-band subtyped user records stay skipped.
- ensureConfigInitialized: keep a settled failure cached (core never
  retries initialization) so later submits surface the real cause, and
  correct the stale re-entry rationale in the comments and design doc.
- Tests: first mount coverage for transcript-view and transcript-adapter,
  the idle-flush hook extraction with its own suite, shell-mode id
  uniqueness, and the fitting-body ctrl-s guard; the gating spec's
  turn-done marker moves onto the waitForScreen channel; revert an
  unrelated SKILL.md formatting drive-by.

* fix(cli): close two round-2 review findings on the OpenTUI surface

- Restore the dim styling of the permission-scope description, which the
  nested-text fix had flattened into the label color.
- Add a direct-drive pin for the cached rejected initialization: core flips
  `initialized` before awaiting the flight, so a re-entry after a settled
  failure reports "Config was already initialized" instead of the real cause.
  The existing submit-path test masks this because its mock rejects with the
  same error on every call.

* fix(cli): restore ink parity on the scope row and the theme preview diff

Two round-3 review corrections, each verified against ink's own code:

- The permission-scope label goes back to the single-string form
  (`${label}    ${description}`, one colour). ink's RadioButtonSelect
  renders the regular path as a single <Text color={titleColor}> and
  PermissionsDialog builds that label with four spaces, so the row-box
  with a dim description diverged twice from ink; the nested-<text>
  shape it replaced never rendered at all.
- THEME_PREVIEW_DIFF's hunk header declared two lines per side and
  supplied one. ink's DiffRenderer tolerates that header; OpenTUI's
  <diff> parser rejects the hunk and the preview pane rendered an error
  string instead of the diff. Counts corrected to -1,1 +1,1; rendered
  output is unchanged because neither renderer displays the header.

* fix(cli): land review round-3 on the OpenTUI surface (13 Criticals + 16 Suggestions)

Criticals: settings sub-dialog routing + arena composer fill owned by the
shell (U-9); confirm-resolved carries the approved/rejected outcome so a
No/Esc decline is not mislabeled; the mid-turn steering restore re-queues
texts when the abort lands behind a resolved hop; displayText replay and
attachment placeholder on /resume; tool-card description stays visible for
confirmation types the dialog does not carry; wide-char rows measured in
display columns for the content caps; followup ghost abort/dismiss split;
Esc in shell mode exits and interrupts in one keypress; status text shows
Shell mode; stale-dropdown guard for the `!` toggle; bidi stripping on
arena rows; boot-seeded auth error keeps Esc dismissible; pending-card
gating.

Suggestions: POSIX wrap closes its brace group on its own line (trailing
comments no longer swallow the wrapper), shell status summary is
cancelled for user aborts, shell lane reuses the dispatcher's admission
rule and passes cwd/signal/config through, pwd/history writes only on the
chat that started the command, `!` child width mirrors ink, expand guard
for short terminals, expanded tail keeps its hidden-rows label, in-place
settings flips invalidate the suggestion effect, MCP fixture pins both
name fields, design-doc coverage paragraphs updated to the extracted hook.

14 remaining Suggestion-level items are recorded as U-39 in #8662.

* test(cli): pin idle slash dispatch over the shell lane in shell mode (U-33)

The mid-turn tag and the drain's shell branch are pinned by the ?btw and
PATH cases; this adds the idle precedence half of the routing.

* test(integration): align mem0 long-confirmation oracle with both renderer legs

The expanded-state oracle asserted a whole-viewport absence of the
hidden-tail label, but OpenTUI's fixed alt-screen viewport keeps the
transcript's own capped copy of the payload (with its label) above the
dialog, so the negative check could never pass on that leg. Scope the
label assertion to the confirmation section there, and keep the
whole-screen form for ink, whose expanded dialog scrolls the section
heading out of the viewport. The OpenTUI leg also needs a taller PTY
(his expand guard only opens ctrl-s when the tail window reveals more
rows than the collapsed head), while ink derives its cap from terminal
height and must stay at the historical 38 rows for the bounded-view
oracle to trigger.

* fix(cli): honor the ctrl-s promise and keep pending payloads approvable

TextBody only offers ctrl-s when expansion actually reveals rows the
collapsed window hides, and labels the expanded view's dropped head rows
as first-N instead of last-N; a pending tool card keeps its own bounded
row budget instead of the settled 5-row cap, so the tail of the payload
being approved stays on screen (round-7 R5-1/R5-2/R5-9).

* fix(cli): hold plain prompts behind running shell commands and harden the shell lane

Four defects around the `!` shell lane and the deferred-command queue:

- A plain prompt typed while a `!` command was running or a slash
  submission was streaming now waits in the deferred queue instead of
  racing the stream, and is routed to the prompt turn when the lane
  frees; slash-form input stays exempt because the dispatcher admits it
  itself.
- A drain pause (a queued turn/dialog that owns the UI) no longer
  re-arms the drain's completion bump, which used to run the queue
  remainder before the turn or dialog it waits for even started; the
  bump still re-arms for entries that arrive mid-drain.
- executeUserShell is async and its seam reports a rejection instead of
  letting it strand the shell gate, and both chat-identity reads are
  guarded so an uninitialized or swapped client can neither throw nor
  inject a stale session's output into a fresh chat.
- One physical `!` keypress toggles shell mode once across press and
  release events.

Pinned by unit tests in opentui-app-shell.test.tsx, shell-mode.test.ts,
input-prompt.test.tsx; every fix is mutation-verified.

* fix(cli): restore steering texts when the abort lands after the hop

An abort that arrives after the streaming hop already resolved used to
fall through the same path as a normal end-of-turn, so the steering
texts the model never saw were consumed without ever reaching it. The
branch now restores the steering queue and only writes the collected
function responses back to history, mirroring the pre-hop abort path.

* fix(cli): let Esc close the auth dialog when boot already seeded the error

When the boot flow failed before any auth type existed, it opened the
dialog with the diagnostic already set. Esc ran the normal handler,
which fell through to the unauthenticated arm, overwrote the boot
diagnostic with the must-connect message and wedged the dialog shut.
Esc now closes first when the displayed error is the boot's own.

* fix(cli): close a dangling line continuation before the shell terminator (R6-8)

A `!` command ending in an odd run of backslashes escaped the appended
`;` into a literal argument, so the wrapped execution ran a command the
user never wrote (`ls \` became `ls ';'`). Close the continuation with a
newline before appending the terminator, in both the ink and OpenTUI
shell lanes.

* fix(cli): carry held-prompt attachments through the drain and re-check the gate between entries (R5-5', R6-4)

Round-5's fix rerouted prompts queued behind a running `!` command into
the drain, but dropped their image attachments on the way: the queue
entry carried only the text. Store the attachments on the entry and pass
them back to onSubmitPrompt at drain time.

And the drain checked the holdsUi gate once before the loop, so an entry
awaiting inside the loop (a `!` command finishing, a dialog opening)
let the remaining entries run against a gate that just closed. Re-read
the gate before each entry and re-queue the remainder paused — same
shape as the holdsUi branch, so the dep change that closed the gate is
what resumes the batch.

* fix(cli): write the completed batch to history on both abort windows, gate mid-turn recordings on delivery, and gate onComplete on the signal (R6-5, R6-7, R6-6)

The R5-4 history write covered only the abort-after-clean-hop window: an
abort landing inside the @-read hop returned restore() and fell through
with no addHistory and no return, so the loop sent a continuation on the
dead signal and dropped the batch's function responses. Hoist the abort
check above the restore branch so both windows write history and return.

The U-32 mid-turn recording was written before the continuation send was
delivered — a send that throws would persist a mid-turn user message the
model never saw and never hand the drained texts back. Stash the
recordings at the sampling boundary and flush them only once the
continuation send yields or completes cleanly; on a throw or an abort
before that, restore the raw texts to the queue instead (ink records
from onDelivered).

And the abort paths inside the prompt generator end with a normal
return, so runTurn's seq guard alone fired a command's onComplete for a
turn the user cancelled with Esc — gate it on the abort signal too. A
decline of every confirmation without Esc stays a completed turn.

* fix(cli): dismiss submitOnAccept ghosts, fold multiline card descriptions, re-arm early-input injection (R6-1, R6-2, R6-3)

* fix(cli): delete unreachable textarea submit route (global Enter handler always wins)

* fix(cli): budget a pending tool card below the confirmation dialog footprint

The confirmation dialog renders in flow beneath the transcript on OpenTUI's
fixed alt-screen viewport (ink scrolls overflow into scrollback). A pending
card left at maxHistoryItemRows (terminalHeight * 4 — 320 rows at h=80)
pushed the dialog's outcome list, hidden-lines label, and ctrl-s hint off
screen: the mem0 interactive e2e timed out waiting for the hint three runs
in a row. The pending description budget is now viewport-aware
(pendingCardMaxRows) and reserves room for the dialog, the transcript rows
above the card, and the card's own chrome; short payloads render in full
(R5-9 preserved) and short terminals fall back to the settled cap.

* fix(cli): budget a pending card for the dialog's expanded payload

The pending-card budget bounded only the dialog's collapsed footprint, so
the mem0 confirmation (whose dialog body duplicates the card's ~3.9k-char
description) expanded past the 80-row viewport and its tail never came on
screen. pendingCardMaxRows now also reserves rows for the payload the
dialog will render, estimated from the card's own description width; the
collapsed-dialog bound keeps MCP-native cards (dialogs without args)
generous, and short terminals still fall back to the settled cap.
2026-09-08 11:31:00 +00:00
Shaojin Wen
bb79319ce5
docs(serve): sync the daemon capability tag count with the registry (#11368)
Two capability tags landed on main in succession and only one of them bumped
the documented count. The worktree-reset tag came with the index update, taking
the documented total to 158; the extension-activation refresh tag arrived
afterwards and left the document alone, so the registry now holds 159. Each PR
was green against its own base — the second one branched before the first
landed — and the contract test only sees the mismatch once both are on main.

The count is asserted against the registry rather than hand-maintained, so this
is a one-character correction with the test as its own verification: red before,
green after.
2026-09-08 09:55:19 +00:00
易良
d7b36db889
feat(core): expand ${session_id} in per-provider customHeaders (#11282)
* feat(core): user-configurable session-ID header for trusted hosts

Gateways like OpenCode Go reject requests without a stable
per-conversation header (x-opencode-session, enforced since 2026-09-06),
and customHeaders cannot carry runtime-dynamic values — the template
route was reviewed and rejected in the outbound-propagation design
(§12.7), which pre-specifies this setting instead.

Adds outboundCorrelation.sessionIdHeader { enabled, headerName,
trustedHosts }: default off with an empty host allowlist, HTTPS-only
exact-host matching, header-name token validation (an invalid name
skips the user branch and keeps the built-in first-party one), and the
enabled flag re-checked at the send site so the path fails closed. The
value resolves per request via the existing wrapFetchWithSessionId
seam, so /new and /resume rotate it without rebuilding the client.
The built-in Routify allowlist behaviour is unchanged.

Closes #10995

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): keep the built-in session_id header independent of the opt-in branch

`buildSessionIdHeaders` called `getOutboundSessionIdHeaderSettings()`
before the built-in short-circuit and inside the shared `try`. A `Config`
collaborator without that method threw, the catch swallowed it into a
debug warning and returned `{}` — so the first-party Routify `session_id`
header that ships today was dropped too. CI caught it: 7 failures across
the four provider suites that build partial `as unknown as Config`
doubles (`AssertionError: expected null to be 'test-session'`).

Rather than teach those four suites about a setting they do not use, make
the branches independent by construction: the opt-in branch now resolves
through `configuredSessionHeaderName()`, which cannot throw. A setting
that ships off is structurally unable to alter the path that ships on.

Also in the same seam:

- Re-check the flag as `enabled === true`, not `enabled !== false`. The
  old test treated a settings object with no `enabled` field as ON, which
  is fail-open — the opposite of what the comment claimed to guarantee.
- Match the trusted host before validating the header name, and warn once
  per distinct name. An invalid name previously logged on every outbound
  HTTPS request, including ones to hosts that were never listed.
- Collapse a configured name that differs from `session_id` only by case,
  since header names are case-insensitive on the wire.

Docs: `docs/design/2026-09-03-outbound-session-id-header.md` still said
the behavior is "intentionally not configurable" with "no user-configurable
allowlist". Updated, and given the threat-model section that §12.7 of the
outbound propagation design asked this follow-up to bring: recipient set,
de-anonymization window, redirect forwarding, misconfiguration, and why a
per-request UUID is out of scope. `settings.md` gains the redirect,
punycode and debug-log caveats; the `headerName` JSDoc no longer
contradicts the regex it describes.

Tests: the built-in header surviving a Config without the getter, an
absent `enabled` field treated as off, and the case-only-difference
collapse.

Not run locally (this box cannot): typecheck, build and vitest are CI's
to confirm. ESLint passes on the changed files.

* feat(core): expand ${session_id} in per-provider customHeaders

Replaces this PR's implementation. The previous approach added a global
`outboundCorrelation.sessionIdHeader` with its own `trustedHosts`
allowlist; this delivers the same user need through the mechanism #10995
actually asked for.

Why the change of approach:

- **`outboundCorrelation` is the wrong home.** It exists (from #4390) for
  correlation data *qwen-code itself* injects, is documented only in the
  developer telemetry docs, and defaults everything to off because it is
  optional. #10995 is the opposite on all three counts: the user
  configures it, they need it in the settings docs, and without it the
  gateway rejects every request. Filing it there repeats the category
  error #4390's review objected to, one level down.
- **A single global `headerName` cannot serve two gateways.** One string
  and one host list mean a user with two gateways wanting different
  header names cannot express it, and two model entries on the same host
  cannot be distinguished at all.
- **`trustedHosts` re-encodes `modelProviders[].baseUrl`.** A global
  setting has no scope of its own, so it has to rebuild one from the
  endpoint the user already wrote down.

`customHeaders` is already per-provider, so scope comes for free: which
hosts may receive the value is answered by the provider entry the header
hangs on, and which providers need it by which entries carry it. No
allowlist, and none of the host normalization, punycode conversion or
wildcard rejection it required.

What is kept from the #4390 review: the consent decision stays in
`outboundCorrelation`, as `allowDynamicHeaderValues` (default false),
because an expanded value carries live session state to a third party.
It also stops a provider preset or extension from quietly promoting a
`customHeaders` entry it ships into an identity header — provenance is
lost once presets and user settings are merged.

Fail-closed throughout: gate off, empty session ID, or a Config that
cannot answer all drop the header rather than putting a literal
`${session_id}` on the wire. Whether any placeholder exists is decided
once at client construction, so providers without one keep the fetch
wrapper's existing early return. Gemini needs its own path: its
customHeaders are frozen into the SDK client options at construction, so
placeholder-bearing entries are kept out of the client entirely and
supplied per request instead.

The built-in first-party Routify branch is untouched, and
docs/design/2026-09-03-outbound-session-id-header.md stays accurate as
written — the two mechanisms are complementary, which is how #10995
described them.

Verified locally: 421 tests pass across the six affected core suites,
including the four provider suites whose session_id assertions the
earlier approach broke. Typecheck, build and the settings-schema
regeneration are CI's to confirm — the schema entry was hand-written with
its description extracted from settingsSchema.ts, so CI's drift check is
the authority.

Rationale and the comparison in full:
docs/plans/2026-09-07-session-id-header-shape-comparison.md

* docs: drop the shape-comparison plan from this PR

The rationale it carried is already in the PR description and the
implementation commit message. The rest of it was review archaeology that
does not belong in the repo.

* fix(core): warn when a configured placeholder is refused by the gate

#10995 treats writing `${session_id}` into a provider entry as the opt-in
("the header is only sent when the user configures it"). With the consent
switch defaulting to off, a user following the issue's example got
silence: the header was dropped and the only trace was a debug log that
is normally disabled. Their observable symptom would be a gateway
rejecting every request with nothing on screen to explain it — the same
failure mode this PR's earlier approach was criticized for.

Warn on the console at client construction instead, naming both the
header and the setting to flip, once per distinct header set. The check
cannot throw: it runs while a provider client is being built, and a
partial Config must not break that.

Docs now lead with the fact that setup is two steps, and say what the
warning looks like.

Also adds the end-to-end coverage that was missing: expansion through the
seam the providers actually construct, the gate dropping the header
rather than emitting the literal, rotation across sessions without a new
client, and a provider with no placeholder keeping the untouched
early-return path.

428 tests pass across the six affected core suites.

* fix(core): close dynamic header consent gaps

* fix(core): close remaining dynamic header gaps

* fix(core): gate session environment aliases

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: 易良 <jinjing.zzj@gmail.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-09-08 09:39:20 +00:00
易良
59163a3ad0
docs(serve): clarify API-only HTTP integration (#11314)
* feat(serve): add --api-profile and an OpenAPI contract for REST integrators

External teams want to build on Qwen Code over HTTP without the Web Shell.
`--no-web` already gives them an API-only daemon, but two gaps remain: the
surface they get is the whole ~110-route set the Web Shell drives, and there
is no machine-readable contract for any of it.

Add `--api-profile=full|minimal`. `full` (default) is unchanged and installs
no middleware at all. `minimal` serves only the partner-facing subset --
session lifecycle, prompting, SSE, permission responses, and read-only
workspace context -- and answers 404 elsewhere.

Implemented as a single gate after the `authenticate` middleware rather than a
conditional on each of the ~65 route registrations: the goal is narrowing the
authorization and contract surface, not saving startup work, so routes stay
registered and simply become unreachable. Placing it after authentication
keeps the 401 uniform, so the enabled surface cannot be mapped by diffing 401
against 404.

The motivation is least privilege as much as ergonomics. Every route shares
one bearer token, so under `full` a leaked token reaches `/workspace/trust`,
extension install, `/workspace/git/push` and `/workspace/settings`. `minimal`
removes those from the routable set entirely.

Also:
- `docs/developers/qwen-serve-openapi.yaml` describes exactly the `minimal`
  surface, with a bidirectional drift guard so the two cannot diverge.
- `/capabilities` reports `apiProfile`. Under `minimal` the "tag present means
  behavior present" invariant does not hold, since `features` still lists
  every tag the build supports; documented as an explicit carve-out, with the
  spec as the authority on reachability.
- Open a `./serve` subpath export. `serve/index.ts` was already written as a
  public barrel for external embeds; the export map just never exposed it.
- `docs/developers/rest-api-integration.md` for integrators, including the
  fact that the daemon spawns `qwen --acp` children and therefore needs the
  CLI on its host.

Plan: docs/plans/2026-09-08-serve-api-decoupling.md

Not verified locally (memory-constrained box): no build, typecheck, or test
run. Lint and formatting were checked on the touched files; the rest is CI's
to confirm.

* fix(cli): type the api-profile gate mock as NextFunction

vitest's Mock<T> collapses NextFunction's overloaded call signatures to
the last one, so `vi.fn<NextFunction>()` produced a Mock that tsc refused
to pass where a NextFunction was required:

  api-profile.test.ts(38,68): error TS2345: Argument of type
  'Mock<NextFunction>' is not assignable to parameter of type
  'NextFunction'.

That broke `tsc --build`, which runs inside the install/prepare step, so
every CI job on the PR failed at "Install dependencies". Use the same
`vi.fn() as unknown as NextFunction` shape the other serve tests use.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-conflict/jmtrj6hmrpn

* fix(ci): satisfy yamllint and register --api-profile in the fast-path guard

`docs/developers/qwen-serve-openapi.yaml` landed in unquoted flow style, which
the repo's yamllint config rejects: 284 `quoted-strings` errors plus 90 `braces`
errors. Requote every plain string scalar and expand the non-empty flow
mappings, matching the style the other non-workflow YAML here already uses
(`packages/live-host/electron-builder.yml`,
`packages/core/src/skills/bundled/computer-use/agents/openai.yaml`). The parsed
document is unchanged, so the `api-profile.test.ts` drift guard reads the same
contract, and Prettier still reports the file clean.

`--api-profile` is a new yargs serve long option that the fast path does not
mirror, so it falls back to the full parser as designed. Register it in the
completeness guard's sample argv and in the fallback set, which is what the
guard exists to make explicit.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-conflict/jmtrlbneopr

* docs(serve): clarify API-only deployment

* Revert "docs(serve): clarify API-only deployment"

This reverts commit 922009d2ba.

* fix(serve): retain and simplify the minimal API profile

* test(serve): guard the minimal profile's two hand-maintained lists

Both lists in api-profile.ts fail open when upstream renames something, and
nothing else in the suite notices:

- MINIMAL_FEATURES filters `/capabilities` by string match, so a renamed tag
  stops matching and the capability silently disappears from the envelope
  while its route keeps working.
- MINIMAL_PROFILE_PATHS is matched against request paths, so a route renamed
  or removed upstream leaves a dead entry that quietly narrows the profile.

Add a guard for each. The path guard walks a real full-profile `createServeApp`
rather than comparing literals to literals, normalising param names so it
checks the path shape the gate matches on, not what a handler calls its
parameter. Both lists are exported for this.

Verified statically against the current tree: all 32 tags exist in
SERVE_CAPABILITY_REGISTRY (160 tags) and all 25 paths resolve to a registered
route, so both guards pass as written. Not run locally beyond that — lint and
formatting only; the suite is CI's to confirm.

* fix(serve): preserve minimal profile prompt capabilities

* revert(serve): drop unnecessary API profile split

* docs(serve): clarify API-only integration

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-08 09:34:22 +00:00
jinye
ab33974509
feat(external-context): Add daemon memory deletion (#11337)
Add an opt-in workspace-bound deletion profile with exact target reads,
full-text and scope verification, single-shot deletion, and absence checks.
Preserve full record identifiers and cover approval and failure boundaries.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-08 09:30:06 +00:00
jinye
272de8ace6
fix(serve): configure live-state polling with a five-second default (#11339)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-09-08 09:08:25 +00:00
BaboBen
cffc40495a
feat(dingtalk): show dynamic lifecycle tags (#10504)
* to #10366 [feat] Show DingTalk dynamic lifecycle tags

Co-authored-by: Codex Using gpt-5.6-sol

* to #10366 [feat] Show lifecycle phases on DingTalk status cards

* to #10366 [feat] Show lifecycle phases in DingTalk card body

* to #10366 [feat] Harden DingTalk lifecycle presentation

- Keep lifecycle cards phase-only and localized
- Coalesce stale reactions and prioritize terminal cleanup
- Resolve the effective language before channel construction

Co-authored-by: Codex Using gpt-5.6-sol

* to #10366 [bugfix] Close DingTalk lifecycle delivery races

- Preserve terminal and latest phase updates across rejected reaction calls
- Keep source labels and current phases stable in truncated status cards
- Await bounded reaction cleanup before channel shutdown

Co-authored-by: Codex Using gpt-5.6-sol

* to #10366 [bugfix] Preserve phases across reaction drain handoff

Reschedule lifecycle reaction draining when a phase arrives during drain teardown.

Cover the no-op drain microtask race and remove an unrelated formatting diff from the PR.

Co-authored-by: Codex Using gpt-5.6-sol <noreply@openai.com>

* feat(dingtalk): refine tool lifecycle phases

* test(dingtalk): cover granular lifecycle phases

* fix(dingtalk): secure lifecycle phase updates

* test(cli): remove duplicate folder trust mock

* fix(channels): clear terminal tool kind cache

* fix(channels): await DingTalk reaction drain at shutdown and harden drain paths (#10504)

- DingtalkChannel.disconnect() is synchronous again; the reaction-cleanup
  drain is surfaced through waitForDisconnect() so disconnectChannels()
  actually awaits it at shutdown instead of exiting with transient tags
  still attached.
- Repair the two vacuous shutdown tests to hang their pending promise off
  the waitForDisconnect hook production awaits.
- AcpBridge heartbeat filter now also drops subagentProgress frames,
  matching the DaemonChannelBridge guard so subagent progress no longer
  resets the phase to Working.
- DaemonChannelBridge restores the remembered kind for kindless terminal
  tool updates (per-session map, retired on terminal status and session
  drop), so daemon-mode tool completions emit lifecycle events instead of
  protocol errors.
- finishReactionState keeps the state when a terminal recall fails and
  finishReaction re-arms the blocked finish, giving the cleanup a second
  chance instead of stranding a stale phase tag.
- Drain failures are latched per transition so a failing emotion API is
  not re-hit once per streamed chunk; a different desired tag still
  retries.
- Correct the design-doc phase list and the two plan-doc status notes.

* fix(channels): bound DingTalk reaction finish retries and retry failed terminal tags (#10504)

* fix(channels): clear DingTalk transient state on standalone bridge exit (#10504)

- Finish transient reactions without a terminal tag and terminalize ticking
  status cards when the standalone ACP bridge process exits, through a new
  ChannelBase onBridgeDisconnected hook wired to the bridge 'disconnected'
  event. Crash recovery restores the sessions on a fresh bridge, so session
  routing state is deliberately left untouched and no outcome is fabricated
  for the interrupted turn.
- Rename the tool-failure presentation phase to 'failed' and label it
  '⚠️ Tool failed' / '⚠️ 工具失败': the runtime makes no retry, so the old
  '⚠️ Retrying' label claimed a recovery that never happens.
- Restrict the Chinese presentation table to Simplified locales (zh, zh-CN)
  so zh-TW and other Traditional locales fall back to English instead of
  being shown Simplified labels.

* fix(channels): wire DingTalk bridge-disconnect cleanup in production (#10504)

The standalone bridge-exit hook was only attached when a channel managed
its own bridge events; every production construction passes a router, so
the cleanup never ran and a crashed turn kept its transient reactions and
ticking status card forever. Dispatch the hook from the crash-recovery
disconnect listener attached to every bridge, and localize the terminal
card copy this path newly reaches for non-Chinese display languages. Also
pin the zh label tables, display-language wiring, kind-map retention, and
reaction routing arguments the review flagged as unverified.

* docs(channels): drop disproved ordering claim from bridge-disconnect comment (#10504)

The comment claimed the transient-state dispatch must run before the
recovery guards can skip, but the guard's skip paths are all shutdown
paths, where disconnect() performs the same cleanup; moving the dispatch
after the guard is byte-identical on crash, SIGINT, and 403+crash
probes. Keep the true why: cleanup is transient-only because crash
recovery restores the sessions.

---------

Co-authored-by: Codex Using gpt-5.6-sol <noreply@openai.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-09-08 08:55:17 +00:00
qqqys
f70aaced58
feat(goal): size the checkpoint verifier timeout for a full claim list, and let operators set it (#11305)
* feat(goal): size the checkpoint verifier timeout for a full claim list, and let operators set it

The checkpoint verifier compresses an evidence window into up to 32
claims of up to 2,000 characters each, with sources, streamed with
thinking off: tens of kilobytes of JSON, minutes at a large model's decode
rate. Its 30 s ceiling fit a small window and abandoned every checkpoint
of the one window compaction exists for, the one that had overflowed the
catalog, so no checkpoint was ever written. Default to 180 s, derived from
the output bound, and expose it as model.goalCheckpointTimeoutSeconds with
the same validation shape as model.goalTokenBudget.

* fix(goal): stream the checkpoint side query and review follow-ups (#11305)

Address the review round on the checkpoint verifier timeout PR:

- Stream the checkpoint verifier side query so the raised ceiling is
  reachable: non-streaming holds all bytes until the JSON is generated,
  so the provider request timeout (default 120 s) aborted and retried
  every attempt past it, leaving any ceiling above it unreachable.
  Streamed, the transport timeout bounds only connect + first response
  and the stream guards apply instead.
- Declare minimum/maximum on model.goalCheckpointTimeoutSeconds so the
  /config write path refuses values the next startup rejects, and
  regenerate the committed settings.schema.json mirror (the freshness
  gate was failing CI's Lint & Static job).
- Correct the three strings that claimed a timed-out checkpoint counts
  as stalled: it settles as an inconclusive check, the stall streak is
  preserved, and a later turn retries (the stall-counting behavior is
  companion PR #11304's change).
- Pin the verifier arming call and the invalid-value debug warning with
  tests, including the silence half for unset/valid values.

* test(cli): add GOAL_CHECKPOINT_TIMEOUT_SECONDS_CAP to partial core mocks (#11305)

The new value import in settingsSchema.ts is evaluated at module load, so
the two suites that hand-write a partial @qwen-code/qwen-code-core mock
without spreading the original module crashed at collect time with
"No GOAL_CHECKPOINT_TIMEOUT_SECONDS_CAP export is defined on the mock".
Add the constant to both mocks, matching each file's existing pattern of
hardcoded constants.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(goal): cap the checkpoint ceiling at a wait the default wire honours

The checkpoint call is streamed, so past the stream lifetime guard it is the
guard that ends the call and the verifier's own timer never fires. A cap of
one hour therefore let the setting validate, and the getter report, a ceiling
no default deployment can reach: an operator raising it to survive a slow
model waited the lifetime cap, got no checkpoint, and saw exactly the
behaviour they had before touching it. Derive the cap from
DEFAULT_STREAM_MAX_LIFETIME_MS instead, and say in the setting, the schema and
both docs that a longer ceiling needs the stream guard raised too -- a knob
that lives in the environment rather than settings.json.

* test(goal): pin the delay the checkpoint abort timer is armed with

The suite proved the exported default is 180 s and that a timer fires when
one is passed, but nothing observed the delay handed to setTimeout: clamping
it back to the old 30 s left every test green while an operator's configured
ceiling silently did nothing. Advance fake timers to one tick before the
armed ceiling and assert the signal is still live, then over it and assert
the rejection, for both the configured value and the built-in default.

* docs(goal): say the checkpoint ceiling is fixed, not raisable by env var

The cap is derived from DEFAULT_STREAM_MAX_LIFETIME_MS at module scope, but
the env override is resolved per pipeline, so lifting
QWEN_STREAM_MAX_LIFETIME_MS does not widen the accepted range: a value above
900 is still refused at startup. Five user-facing strings said the opposite
-- the constant's docblock, the settings schema description, the validator's
error, and both docs -- so an operator who raised the guard would retry the
setting and be rejected again with advice they had already followed.

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-09-08 08:39:17 +00:00
callmeYe
8f121742ac
refactor(daemon): decouple extension activation refresh (#10991)
* refactor(daemon): decouple extension activation refresh

* test(daemon): update activation capability expectation

* docs(daemon): clarify activation convergence window

* fix(daemon): correct activation docs and guard stale refresh message

* fix(cli): route turn-index reads through runtime-root pin (#11036)

(cherry picked from commit eaef97e634)

* fix(web-shell): keep the activation refresh out of the mutation path (#10991)

The explicit refresh submitted after an activation commit produced three
measured defects on the previous round's code:

- The daemon records it as a nameless `refresh` operation, and the mount-time
  recovery effect adopted any non-`install` operation as a resumable mutation.
  Remounting the panel while the refresh was in flight locked every control
  behind an action the user never started, then certified it as queued.
- The workspace-qualified refresh route validates a supplied client id against
  only the targeted runtime and then discards it, so a session attached to
  another workspace drew a spurious 400 over an activation that had already
  committed. Omitting the id also matches this page's sibling activation calls.
- Arbitrating the refresh failure through the shared message slot needed a
  claim counter that both let a stale rejection clobber a catalog load error
  and silently dropped genuine refresh failures bumped by unrelated actions.
  A dedicated notice replaces the counter, so the two messages coexist.

* test(cli): widen the serve import-boundary budget for shared-host contention (#10991)

The dev entrypoint pays a cold tsx transform of the whole CLI graph before
the daemon can listen, so this wait is CPU-bound rather than a fixed cost.
Measured on a 64-core shared self-hosted host: 12.6s idle, and 88.4s under
the 16-worker contention of a full `vitest run --changed` pass, which is
past both the 30s default budget and the 60s one the ECS branch allows.

The wider budget was gated on RUNNER_NAME starting with `ecs-qwen-`, but
other shared pools do not set RUNNER_NAME at all, so the relief never
reached the hosts that need it. Give every host the contended budget and
drop the branch that cannot identify them.

The assertion itself is unchanged: the import-boundary checks on the child's
stderr still fail the test immediately, and a genuine hang still fails, just
later.

* fix(web-shell): gate the activation refresh on trust and its notice on owner (#10991)

The explicit refresh submitted after an activation commit produced two
measured defects on the previous round's code:

- The user-scope activation route carries no workspace-trust gate, but the
  compensating refresh route requires one, so every global activation in an
  untrusted workspace painted a "session refresh failed" banner over an
  operation that had succeeded; the reconciler is the only path that can
  serve an untrusted runtime. Keep the projection's `trusted` flag in page
  state and skip the refresh when it is false.
- The dedicated refresh notice was a bare string with no owner and exactly
  one clear site, so it outlived the action it qualified and rendered on
  other extensions' detail panels. Carry the triggering extension's name
  with the error, gate the detail-view copy by owner (the list copy stays
  ungated), and clear it where the sibling action paths already reset the
  message triple.

* fix(web-shell): decide the post-activation refresh on freshly observed trust (#10991)

The trust guard evaluated the render-time workspaceTrusted snapshot after
awaiting load(true), so the first activation following an out-of-band
trust change decided on the stale value; load() now resolves the trust it
observed (null when the projection fails, falling back to the last known
value) and the guard reads that. Also tag each refresh submission with a
recency token so a rejection from a superseded refresh no longer evicts
the still-current failure banner. Document that the synchronous refresh
route does not record the applied generation, unlike the asynchronous
operation form.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(daemon): scope activation-refresh guidance to per-workspace coverage (#10991)

* fix(daemon): scope global-default batch claim to inherited default in activation docs (#10991)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(web-shell): pin the remembered-trust fallback in activation refresh (#10991)

Cover the branch where the post-activation reload loses the activation
projection: the refresh decision must fall back to the last known trust,
not default to trusted. Mutation-verified against setWorkspaceTrusted
being forced true/negated and the ?? workspaceTrusted fallback being
forced true.

Also restate the skipRefresh reservation-guard comment as the invariant
it enforces rather than a consequence no current route can trigger.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: 易良 <1204183885@qq.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
2026-09-08 08:39:06 +00:00
易良
32f51a330b
chore: restore regression coverage after WebUI retirement (#11101)
* chore(web-shell): follow up #9812 review findings and deferred suggestions

Address the remaining review suggestions from the @qwen-code/webui
retirement (#9812) and the deferred items tracked in #11076:

- Fix the 01-architecture.md adapter diagram path (missing session/).
- Correct the followup-suggestions doc: generation fires on end_turn, and
  headless/SDK daemon clients should opt out to avoid the per-turn LLM cost.
- Pin playwright to 1.61.1 to restore version parity with @playwright/test.
- Restore file: scheme rejection coverage in Markdown.test.ts.
- Pin the historical sessionStorage key prefix with a literal assertion.
- Cover the legacy exported JSONL rejection and ChatRecord happy path.
- Restore the shell-substring classifier trap with a live path.
- Point useDaemonFollowupSuggestion docs at ChatEditor, not the deleted InputForm.
- Replace the theme-toggle timing-race assertion and exercise the
  unhandledrejection fail-closed path in the document browser gate.

Closes #11076

* fix(test): poll the theme class instead of a matcher vitest lacks

The browser gate imports expect from vitest and drives playwright's
Locator directly, so toHaveClass does not exist on the assertion and
typecheck:integration failed with TS2339 on this line. Poll the class
attribute through the same expect.poll idiom the neighbouring
assertions use, which keeps the retry the assertion was switched to
and needs no @playwright/test dependency.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-conflict/jmtooas8sln

* fix(test): assert the exported theme class exactly

`document-main.tsx` toggles `dark` and `light` on <html> mutually
exclusively and nothing else writes `documentElement` classes, so the
attribute is always exactly one of them. Matching `/light/` as an
unanchored substring would also accept `dark light`, which is the
theme-toggle breakage this poll exists to catch.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtow5pgslz

* fix(test): realpath the exporter main check and pin its JSONL guard

Node realpath-resolves `import.meta.url` for the ESM main module but
leaves `process.argv[1]` as invoked, so comparing the raw spellings made
a symlinked invocation skip `main()` and exit 0 having written nothing —
a silent no-op the harness reads as success. Compare realpaths instead
and export the predicate so both directions are pinned.

Also extract the input gate into `assertRenderableJsonl` so the legacy
exported-JSONL rejection can be asserted directly: the previous tests
only pinned the two predicates feeding it, so deleting the guard and
losing the remediation hint kept every test green.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtow5pgslz

* docs(followup): name the real ACP stop reasons, drop "unconditional"

The parenthetical this PR added named `interrupted` as a daemon stop
reason. The ACP `StopReason` union is
`end_turn | max_tokens | max_turn_requests | refusal | cancelled`, so
there is no `interrupted`; an interrupted turn reports `cancelled`. Name
the four suppressing values the union actually defines so an integrator
writing `stopReason === '...'` gets a branch that can fire.

"unconditional" was wider than the code and contradicted the condition
list on the same page: `Session.ts#maybeEmitFollowupSuggestion` returns
early on the stop reason, the todo stop guard,
`ui.enableFollowupSuggestions`, PLAN approval mode, a missing chat, and a
non-model last history entry. Point at those conditions instead; the cost
advice still holds because the `enableFollowupSuggestions === false`
return sits before `generatePromptSuggestion` is reached.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtp6vibomd

* test(web-shell): drop the duplicate historical-key assertion

`persists under the literal historical key prefix`, added on this branch,
became a byte-equivalent twin of `writes under the historical WebUI key`,
which reached main with #11107 and arrived here through the merge at this
PR's head: same `persistStableClientId('client-a', 'session-a')`, same
literal `qwen-code-webui-client-id:session:session-a`, same expectation.
One fact was reported under two names, so a prefix rename reddened two
tests and an auditor could not tell which copy was load-bearing.

The surviving test's comment already carries the rename rationale, so
dropping the copy loses no coverage.

Verified with `cd packages/web-shell && npx vitest run
client/daemon/session/clientLifecycle.test.ts`: 19 passed before, 18
passed after. Mutating SESSION_CLIENT_ID_STORAGE_PREFIX to
`qwen-code-webshell-client-id:session:` after the dedupe still reddens
3 tests (15 passed), including the surviving `writes under the historical
WebUI key`; reverting the mutation returns to 18 passed.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtp6vibomd

* test(integration): drop the unreachable unhandledrejection arm

The block this PR added after the CDN-failure loop could not reach the
`window.addEventListener('unhandledrejection', showLoadError)` handler its
comment claimed to exercise. `page.route('**/*', route =>
route.abort('blockedbyclient'))` fails the `<script id="transcript-renderer">`
load, the template's capture-phase `error` listener (document-index.html:45-56,
matching `event.target.id === 'transcript-renderer'`) calls `showLoadError()`
first, and `setContent(..., { waitUntil: 'load' })` only resolves after that
subresource has failed - so `data-render-complete='error'` and the alert text
were already in place before `page.evaluate` dispatched anything, and
showLoadError's one-shot guard (`if (document.body.dataset.renderComplete)
return;`, document-index.html:30) made the synthetic rejection a no-op. The
block was a strict duplicate of the loop's own `rendererBody === null`
iteration: identical all-abort route, identical two assertions.

Exercising the handler for real needs a renderer body that passes the
document's pinned SRI integrity and then rejects during boot; no body this gate
can serve passes SRI, so the arm cannot be made honest here. Deleting it
returns the file to origin/main's state for this test - the handler and its
missing coverage are both pre-existing on main (packages/web-templates is not
in this PR's diff), so no coverage this PR was responsible for is lost.

Verified: `npm run typecheck:integration` reports 0 errors in this file (6
remain, all in packages/** against generated/dist paths absent from an unbuilt
worktree: channels/feishu, cli/src/generated/git-commit.js,
web-templates/src/generated/*); `npx prettier --check` and `npx eslint` clean
on the file. The browser gate itself was not executed locally - it needs the
full build chain plus Chromium.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtpfg5ewmp

* ci(review-context): route concurrent-runner changes to the helper-tests lane

This PR added
`integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.test.mjs`,
the first `node:test` file under `integration-tests/`, and wired it into CI
through the `HELPER_TESTS` env list in `.github/workflows/ci.yml`. The tracked
review-context manifest still mapped the whole `integration-tests/**` path to
`recommendedTests: ['integration-tests']`, whose vitest lane collects
`**/*.test.ts` only (`integration-tests/vitest.config.ts:21`), so a future PR
touching only the exporter and its `.mjs` test is told to run a lane that
cannot collect the file - the same silent drop `HELPER_TESTS` exists to
prevent.

Add a narrower rule for `integration-tests/concurrent-runner/**` recommending
`helper-tests`. Matching rules merge rather than first-match, so the lane list
for that directory becomes the union and every other `integration-tests/` path
is untouched. The pinned `expectedManifest` in
`manifest-repository-context.committed.test.ts` moves with it, which is what
that pin is for.

Verified through the real provider (tsx importing
`packages/cli/src/commands/review/lib/manifest-repository-context.ts`) because
the packages/cli vitest globalSetup gate refuses to run on this unbuilt
worktree:

  concurrent-runner .mjs           -> ["helper-tests","integration-tests"]
  concurrent-runner .js            -> ["helper-tests","integration-tests"]
  chat-transcript-document.test.ts -> ["integration-tests"] (unchanged)
  all 10 rules co-matching         -> non-null, 80 relatedPaths, and every
                                      `paths` probe matched some rule

Also checked the committed manifest deep-equals the test's pinned literal (10
rules on both sides), and that dropping the new rule breaks that equality, so
the pin is not tautological. `npx prettier --check` and `npx eslint` clean on
both files.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtpfg5ewmp

* fix(deps): sync pnpm lockfile for Playwright pin

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-08 08:15:03 +00:00
易良
078b924989
refactor(ci): extract release workflow scripts (#11165)
* refactor(ci): extract release workflow scripts

* test(ci): pin release-note labeling to the extracted step script

release.yml no longer inlines the auto-labeling body: the publish step now
just dispatches to run-release-step.sh, which owns the git rev-list
enumeration and the classify-release-notes.mjs call. The wiring test kept
asserting those four strings against the step's run block, so it failed on
the dispatcher path alone.

Read run-release-step.sh and assert the labeling body there, and assert the
dispatch from release.yml. The workflow-level guarantees (step name,
continue-on-error, GITHUB_TOKEN env, issues/pull-requests write permissions)
and the .github/release.yml exclusion entry are unchanged.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-conflict/jmtpmlcodmz

* test(ci): repoint release assertions at the extracted step script

The extraction moved the GitHub Release notes, standalone archive, and
release-failure autofix label logic out of .github/workflows/release.yml
into .github/scripts/run-release-step.sh, but three consumer suites still
pinned the moved text in the YAML, so `npm run test:scripts` was red and
the release workflow's fail-closed `quality` gate would refuse to publish.

Read the step script in ai-release-notes-workflow, install-script, and
qwen-autofix-workflow and assert the moved strings there with their new
lowercase locals (`notes_args`/`notes_file`), keeping on release.yml only
what genuinely stayed in it. Restore the "Safe to auto-apply approval"
rationale above both AUTOFIX_APPROVED_LABEL writes in the notify-failure
arm; it is the only recorded justification for the workflow granting
itself autonomous autofix approval.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtq0vr74ni

* test(ci): derive the ECS wipe subject set from the runner, not the wipe

Selecting checkout jobs by "has a Restore workspace ownership step" made
the subject set a function of the property under test, so a job added to
the ecs-qwen-hk4-host pool with an actions/checkout and no wipe was
filtered out before any assertion ran. Key the exemption on the pool
marker instead, and pin that every pool-routed job carries the wipe
whether or not it checks out.

The hosted label cannot be the discriminator: every pool-routed runs-on
expression names 'ubuntu-latest' as its fallback branch.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtq0vr74ni

* test(ci): re-scope the release-creation token pin to its own step

The workflow-wide toContain was satisfied by the second CI_BOT_PAT
occurrence on "Trigger ECS runner qwen update", so flipping the token on
"Create GitHub Release and Tag" to github.token left every lane green.
Restore the step scoping and the deleted rationale comment in release.yml.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtqfvvmto2

* fix(ci): fail the release step runner closed on pipeline legs

run-release-step.sh set bare `set -e` while both sibling runners set
pipefail. In notify-failure a connection-level gh failure inside
`gh issue list ... | jq -c ...` leaves jq exiting 0 on empty input, so the
empty result took the create-a-new-issue branch and filed a duplicate
autofix/approved release-failure issue past all three reuse guards.

Pin the flag next to the existing sibling pin and add a behavioural row for
the notify-failure arm with an unreachable gh.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtqfvvmto2

* test(ci): relocate the release timeout value-format contract

This PR condensed the nine-line release.yml lane comment to "values are
minutes" and relocated the format half nowhere: grep for the deleted claims
(expression error / free text / truthy / leading number) found no copy under
.github/, scripts/tests/ or docs/. Put the contract beside the assertions that
already pin the three tunable lanes' expressions, since release.yml is
size-pinned twice.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtqfvvmto2

* docs(ci): scope the workflow_sha claim to the extracted helpers

The design doc said the trusted checkout "prevents an operator-selected
release ref from supplying code that receives release credentials". Two paths
at HEAD contradict that reading: resolve-version executes the ref's
scripts/get-release-version.js under the job token (run-release-step.sh:84),
and push-release-branch commits with core.hooksPath .husky (:116, :130) in a
step whose env carries CI_BOT_PAT (release.yml:659). Both predate this
extraction, so name them as residual paths rather than as prevented ones.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtqfvvmto2

* fix(ci): format the release-version test to satisfy the Prettier gate

`Lint & Static` failed at the `Run Prettier` step: a continuation line in
the `assert-unreleased` format-gate test carried four extra spaces.

* fix(ci): exclude nested .git dirs from the docker build context

The extraction added a `.release-workflow/` checkout to `integration_docker`
between the sandbox build's reset and the build itself, and `.dockerignore`
listed `.git` only in its root-anchored form. A depth-1 sparse checkout of
the pinned ref is 1.1M of worktree over 51M of git objects, so every
uncached sandbox image baked those objects into a discarded builder layer on
the shared pool this job is routed to. `node_modules` and `dist` already
carry both forms.

* fix(release): close four gaps the script extraction left behind

R1-15: the push-time guard returned exit 2 for a malformed version, and
run-release-step.sh treats 2 as a transient probe failure — so a refusal
that can never succeed was retried three times over ~45s and logged as a
connectivity problem. Malformed versions now exit 4, which the retry loop
breaks on immediately and which is not the exit-3 version_refusal marker.

R1-16: the extraction moved the guard behind `.release-workflow/` so the
decision comes from the workflow-pinned SHA, but left a working
`--assert-unreleased` dispatch in the ref-supplied get-release-version.js.
A future step using that spelling would load the selected ref's guard.
Removed it, and re-pointed its five tests at the real entry point.

R1-13: assert-release-version.mjs ships alone under release.yml's
sparse-checkout set, so it cannot import release-helpers.js — the
duplicated isExpectedMissingGitHubRelease is deliberate. Documented that
at both sites and pinned the two bodies identical, so widening only the
guard's copy (which would read a rate-limited probe as "release absent")
goes red.

R1-6: restored the notify-failure arm's rationale, matching the condensed
comments the other five arms kept: the exact-title re-check that stops a
v0.18.1 failure reusing a v0.18.10 issue, the bot-author preference, and
the re-query that keeps autofix off a maintainer-owned issue.

* fix(release): re-pin the trusted runner before every step that runs it

R1-10: `Build Bundle and Prepare Package`, `Build Standalone Archives` and
`Verify Standalone Archives` executed `.release-workflow/.../run-release-step.sh`
from a tree that selected-ref npm code had already run over — `build-package`
for the first two, and `npm publish`'s own lifecycle scripts for the third,
inside the job that holds `id-token: 'write'`. At the merge base these were
inline `run:` bodies sourced from the workflow file, immune to anything
written into the working tree, so the exposure is introduced by the
extraction. Each now gets the reset-and-re-checkout pair the design doc
already promises for credential-bearing steps.

The guarding test hand-listed six protected step names, so it could not fail
for the three it omitted. It now sweeps every step whose `run` mentions
`.release-workflow/` (17 today) and separately pins that each re-checkout is
preceded by a reset.

R1-5: nothing tied the publish allowlist to the guard's `PUBLISHED_PACKAGES`
except a comment pointing at publish steps this PR deleted from release.yml.
A channel added to the loop but not the array would ship unprobed, so a retry
of a partial release reports "unreleased" and force-pushes over the tip the
shipped package anchors to. The two sets are now pinned against each other,
and the stale comment points at the real pin.

The three inserted step pairs grow release.yml by 294 bytes (well inside the
4096-byte allowance); the ratchet baseline and the PR description move with
it rather than letting the recorded number drift.

* test(release): pin the flags, arms and modes the extraction left unwitnessed

R1-19: the arm's only executable probe stubbed npm with a script that
recorded `$PWD` and discarded every argument, so deleting `--tag=${NPM_TAG}`
stayed green while `npm publish` would fall back to the `latest` dist-tag —
handing the 21:00 UTC nightly to every end-user install and to the ECS fleet
updater. The stub now records the argv and each publish is checked for
`--access public` and the dist-tag.

R2-3: `resolve-version` was the one arm no test executed or text-pinned;
flipping `--type=preview` to `--type=stable` survived all 2233 scripts tests,
while at runtime the stable path ignores `preview_version_override` and would
publish an operator's manual preview as a stable release. Two execution tests
now cover the preview mapping and the malformed-version rejection; both were
run against the real arm before pushing.

R1-18: release.yml invokes these scripts by bare path, so the executable bit
is load-bearing, but every test invokes them as `bash <script>`, which
ignores the mode — and the workflow triggers only on schedule/dispatch, so no
pull-request lane runs the bare-path form. A mode-normalizing commit would
keep the suite green and kill `set-flags` with exit 126. The recorded git
mode is now pinned for all three scripts.

* test(release): pin the trusted prefix, the fork gate and the real version formats

R1-3: every arm pin elsewhere is a substring match that a bare
`.github/scripts/run-release-step.sh` invocation would satisfy just as well,
so nothing pinned the mechanism the extraction exists for. All 17 step
invocations are now required to carry the `.release-workflow/` prefix and
forbidden from naming the release-ref copy.

R1-9: `PUBLISH_AUDIO_CAPTURE` replaced a deleted step-level
`if: github.repository == 'QwenLM/qwen-code'` gate and, unlike its
`PUBLISH_EXTERNAL_CONTEXT_MEM0` sibling, had neither its gate expression nor
its true branch pinned — a fork running this workflow would publish
@qwen-code/audio-capture. Both halves are pinned now.

R1-14: the format gate is the guard's only genuinely new behaviour and its
accept path was exercised solely with '1.2.3', while the two scheduled
releases produce nightly and preview strings. Both formats are now asserted,
so tightening the pattern fails here rather than at 21:00 UTC.

* fix(release): keep failure reporting alive when the trusted runner is not

R1-11: `notify_failure` gained a checkout of `github.workflow_sha` as its
first step, and its whole body now lives in a shell script. At base the job
had no checkout at all and the notifier was inline, so the job whose only
purpose is reporting every other job's failure acquired two ways to fail
silently: a degraded git backend takes out the checkout, and a syntax error
or dropped exec bit in run-release-step.sh takes out every arm including
notify-failure. In both cases `Create Issue on Failure` carried the default
`if: success()` and was skipped — no issue, no autofix label, no dispatch,
and the scheduled autofix fallback scans issues, so nothing recovered it.

Rather than moving 105 lines of notifier back into the workflow — which the
orchestration guard in this PR forbids at 12 run-lines per step — the primary
path keeps the extraction and stops being load-bearing: it runs under
`always()`, absorbs its own failure, and a five-line inline fallback files a
plain issue whenever it did not succeed. The fallback depends on neither the
checkout nor the script, does no reuse, labelling or dispatch, and accepts a
duplicate issue as the cost of never going silent.

The workflow's line ceiling moves 800 -> 830 for those lines, with the reason
recorded at the assertion; the per-step run cap that keeps logic out of the
YAML is unchanged and still holds. Ratchet baseline and the PR description
move with the file.

* style(release): apply the pinned Prettier to the fallback notifier

Prettier 3.6.1 normalizes the quoting on the new `if:` and drops a
trailing space; the ratchet baseline follows the two-byte change.

* docs(release): name exit 4 where the guard's exit codes are described

Adding the malformed-version code left two descriptions behind: the retry
loop had no statement of the contract at all, and the test comment beside it
still said exit 0 and exit 3 were the only decisive outcomes.

* fix(release): avoid duplicate fallback failure issues

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: yiliang114 <jinjing.zzj@gmail.com>
2026-09-08 07:09:09 +00:00
callmeYe
0d1e0fbfa6
feat(core): configure model reasoning capabilities (#10999)
* feat(core): configure model reasoning capabilities

* fix(core): validate configured reasoning metadata

* fix(core): preserve configured model casing

* fix(core): retain case-variant model entries

* fix(core): decide reasoning-capability validity in one shared parser (#10999)

The request pipeline read ModelConfig.capabilities.reasoning straight off the
resolved model while every reasoning control validated it first, so a
capability the pickers refused still reshaped the wire. With disableField
missing -- the capability's only member that has no fallback -- a tier the UI
never offered was dropped instead of shipping, and an unparsable
canDisable: false flipped thinkingMandatory and swallowed an explicit
thinking-off. One parser in core now decides validity for both sides, and an
incomplete entry leaves the model on its pre-capability behavior everywhere.
That removes the CLI's duplicate validator rather than adding a second one.

A reasoning object the user put in samplingParams also went through the
capability mapping, breaking the verbatim contract
clampConfiguredReasoningEffort already keeps: the tier was deleted before the
provider hook could translate it. It now passes through untouched.

Both effort pickers mapped a configured tier the resolved model does not
expose onto index 0, which read as "this tier is current" and let a bare Enter
persist it over the stored global setting; they now say the tier is
unavailable instead. /effort <tier> on a model with no tiers reports that
rather than an empty "Choose one of:" list.

Every new guard has a mutation witness: removing the pipeline parser, the
samplingParams guard, either disableField branch, the canDisable clause, the
dialog out-of-range handling, the efforts prop, or the hoisted zero-tier check
each reddens the test that pins it. The two closed partial mocks of the core
package now supply the real parser so the acpAgent reasoning paths stay
exercised.

* fix(cli): route the turn-index handler through the runtime-root choke point (#10999)

* fix(core,cli): reject malformed reasoning capabilities (#10999)

The shared parser cast three capability members through unchecked while
every consumer reads them strictly, so a mistyped value in an unvalidated
`modelProviders` settings entry silently changed the declaration instead
of failing it. A truthy non-boolean `toggleOnly` collapsed a declared
ladder to toggle-only: the pickers refused every tier while `disableField`
still reshaped the wire. `efforts: []` passed `[].every()` and emitted a
zero-tier capability that suppressed the manifest fallback and stripped
the effort from the request. A non-`false` `canDisable` degenerated to
absent, so a route declaring that disabling is forbidden still shipped the
disable shape that declaration exists to prevent. All three are now
rejected rather than coerced, along with a repeated tier, which duplicated
picker rows, ACP options and React keys. Well-formed input keeps the
identity contract.

Both effort pickers forced the cursor onto the first tier when the stored
global `model.reasoningEffort` is not one the active model exposes, and
confirming without moving persisted that tier over the stored value -- the
rewrite measured live as F2. A forced cursor now cancels in the ink dialog
and closes without writing in the OpenTUI one; navigating first still
selects, and the unset case is unchanged. The OpenTUI footer discloses the
unset case too, matching ink.

Four output-style dialog tests drove keys before the effect deriving the
cursor from the async catalog had run, so that derivation could overwrite
the navigation and pick the wrong row; they now wait for it.

Every new guard condition has a mutation witness: removing the toggleOnly,
canDisable, empty-efforts or uniqueness check, the ink cursor-moved or
stored-tier condition, or the OpenTUI forced-cursor condition each reddens
the test that pins it, and acceptance of a well-formed `canDisable: false`
or `toggleOnly: false` is pinned as well.

Not changed here: the pipeline still drops a carried-over tier the
capability does not list instead of clamping it onto the ladder. The
maintainer's real-stack A/B records refusing an unsupported tier as
correct and files the carried-over case as a picker problem, and the
author's triage answer names the drop as the requested migration contract,
so the clamp stays a maintainer call and its thread stays open. The
turn-index per-request settings test is deferred: this round's merge of
main absorbed the identical handler fix (#11036), leaving only the roster
docstring here.

* fix(core,cli): address round-6 review findings on model reasoning config (#10999)

- Gate the capability disableField emission on non-qwen-family wire
  models: the qwen-family branch above already emitted the route-correct
  disable shape, and re-emitting the declared field after it resurrected
  the deleted top-level enable_thinking (or paired it with
  reasoning_effort: 'none' on tiered DashScope).
- Key capability resolution on the registry base URL with an unscoped
  fallback at both the picker and the wire, so an env/flag/settings
  endpoint override no longer hides a declared capability from one
  surface while the other still advertises it.
- Derive the effort dialog's explicit-choice signal from the user's own
  input (digit key, navigation key, or click via a new onSelectIntent)
  instead of highlight state, which pointer hover also moves; confirm
  single-row lists instead of cancelling them; latch the OpenTUI twin's
  forced-cursor guard on movement rather than cursor position.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* revert: remove review-driven scope expansion

* fix(cli): keep runtime snapshot request overrides on ACP auth refresh (#10999)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* Revert "fix(cli): keep runtime snapshot request overrides on ACP auth refresh (#10999)"

This reverts commit 4f20aa9b2b.

* fix(cli): keep snapshot and route-model request overrides on reasoning paths (#10999)

- acpAgent: resolve the reasoning configuration through the guarded
  helper on auth refresh, so runtime snapshots and duplicate-id route
  models keep their own extra_body/samplingParams overrides (restores
  the merge-base behavior; the route leg was believed unreachable).
- Session: gate reconcileReasoningSelection on an active runtime
  snapshot, matching getDefaultReasoningConfig and the ACP helper.
- pipeline: skip the capability flatten on OpenRouter routes — the
  gateway reads only the nested reasoning parameter, so flattening to
  reasoning_effort stopped delivering the configured tier.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): keep extra_body reasoning and qwen-family disable shapes on capability routes (#10999)

Two capability-mapping fixes on the OpenAI pipeline:

- Exempt extra_body.reasoning from the effort flatten beside
  samplingParams.reasoning. extra_body merges after the mapping inside
  the provider hook, so flattening the configured tier made the hook's
  no-clobber guard keep the pipeline's own reasoning_effort and delete
  the user's extra_body override.
- Re-land the qwen-family gate on the disableField block that
  f948ff6f41 removed. The family branch above already emits the
  route-correct disable shape for qwen wire models; re-emitting the
  declared field resurrected the deleted top-level enable_thinking or
  paired it with reasoning_effort: 'none' on tiered DashScope.

* Revert "fix(core): keep extra_body reasoning and qwen-family disable shapes on capability routes (#10999)"

This reverts commit 540289f37d.

* Revert "fix(cli): keep snapshot and route-model request overrides on reasoning paths (#10999)"

This reverts commit 8f1a7c04a2.

* fix: preserve provider reasoning ownership

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-08 07:05:59 +00:00
qqqys
bbc8ea6488
feat(goal): pause a Goal after three autonomous turns that make no progress (#11239)
* feat(goal): pause a Goal after three autonomous turns that make no progress

An autonomous Goal had two bounds and neither noticed one going in circles.
The token budget is the only thing `queueContinuation` stops for, 30M tokens
by default, and the checkpoint stall streak measures the opposite problem: it
fires when a Goal produces too much evidence to catalogue, and a Goal
producing none never advances it at all. So a model that answers each turn
with a paragraph of status and calls no tools continued until the budget ran
out, with nothing on the record that could ever end the loop.

A Goal now pauses after three consecutive autonomous turns that recorded no
evidence-bearing tool result and proposed no terminal state. `get_goal` and
`update_goal` results do not count -- they carry `provenance: 'goal_runtime'`,
and a turn that only re-reads its own state is the clearest form of the
idling in question. Only the runtime's own continuations are measured: a turn
carrying the user's text is the user steering and restarts the streak, and so
does a user turn reserved while the third quiet turn was still running. The
wind-down hand-off is exempt from both halves.

The streak lives on the record as `noProgressTurns`, exactly like
`checkpointStalls`, so a restart cannot launder it, and edit, replace, and
every resume clear it -- resuming is the user asking for another run at the
objective, not for its last turn. The count comes from a new optional
`takeGoalTurnToolResults` on the renamed `GoalTurnLedger`, fed by the recorder
where tool results are already stamped with the Goal permit. Optional is
load-bearing: a ledger that cannot answer switches the bound off rather than
reporting every turn as idle, and the bound reads the count measured on this
turn rather than the one on the record for the same reason.

The stop is a pause, not `blocked` or `usage_limited`: nothing was spent and
there is no evidence to cite, the Goal simply stopped producing. Following the
pause-reasons design, no new `GoalStateCause` is introduced and no host
changes -- every surface already renders a paused Goal's `lastReason`.

* fix(goal): read an uncountable tool-result answer as unmeasured

A ledger that returns something other than a finite count was read as
zero, which spends one of the three turns on a measurement that never
happened. It now says the same thing as a ledger that throws: the bound
is off for that turn.

Also pin that the turn which spends the streak takes no checkpoint --
the guard was in the code with nothing holding it there -- and point the
design doc's verification list at the pull request's evidence rather
than at an ignored path.

* fix(goal): let the no-progress bound yield to a waiting turn, a spent budget, and a stalled checkpoint

Three of the bound's neighbours outrank it, and the pause write opened a
window where a fourth was lost.

A user turn reserved while the pause record was being appended was
discarded: `beginTurn` is synchronous and does not queue, the guard read the
reservation once before the await, and the later `queuedTurnKey` assignment
erased it. The guard now re-reads the reservation after the write and
declines the pause when one appeared, the way `stopForSpentBudget`
re-validates after its own write. The journal may then hold a `pause` record
the runtime never adopted; that is the shape the budget stop already leaves,
and the conservative side to land on -- a restart recovers a paused Goal with
its reason, and resume is the whole remedy.

The bound also stands down on the turn where a better-fitting limit
coincides with it. A spent token budget reaches the continuation gate, which
grants the wind-down hand-off and stops as `usage_limited`/`token_budget`
instead of an idle pause with no `limitKind`. A Goal carrying a checkpoint
stall streak lets its checkpoint run, so the stall breaker stops it with the
evidence-catalog reason instead of a pause whose remedy would resume it into
the same overflowing window.

The paused snapshot is now built by the same `settledSnapshot` helper as the
usage-limited one, so the settled shape lives in one place.

Tests: a reservation landing inside the pause write is served; a restored
streak at the limit is not spent by a turn the ledger cannot measure; the
stall breaker wins three quiet overflowing turns; the budget wins the
crossing turn. Each goes red under the corresponding mutation.

* fix(goal): keep the no-progress pause reason host-neutral and accurate

The reason is runtime-emitted and headless-reachable, so it must not point at
a slash command a process that has already exited cannot run; and it said
"no tool results" about a Goal whose transcript shows three turns of
`get_goal`/`update_goal` results, which are recorded but do not count. It now
says "nothing to judge", offers resume, and offers edit only together with
resume -- editing a paused Goal leaves it paused. The user doc says the same
and names the bookkeeping reads that do not count.

The constant joins the headless-register test, which now also rejects any
`/goal ` mention, and the shared-constant validation loop.

* fix(acp-bridge): replay only a typed pause as the user typing it

The replay projected every `pause` record as the user typing `/goal pause`.
The no-progress bound is the first `pause` the runtime writes with nobody at
the keyboard, so a replayed session attributed the autonomous stop to the
person who was away. A `pause` now replays as a user message only when its
reason says the user typed it (`GOAL_PAUSE_REASON_COMMAND`, exported on the
goal wire) or when the record predates pause reasons; every other pause
replays as the paused card alone, which carries the reason. `resume` and
`clear` are unchanged.

* fix(goal): preserve steering checkpoints and pause feedback

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
2026-09-08 06:44:48 +00:00
易良
73af280034
feat(channels): add shared multiline instructions field to channel management (#11082)
* feat(channels): add shared multiline instructions field to channel management

Every manageable built-in channel type now exposes a shared optional
instructions descriptor (kind string, multiline), and the Web Shell
channel editor renders multiline string fields as a textarea. The
runtime already consumes config.instructions per channel session;
this closes the management gap so the value can be set and reviewed
through the editor and management API instead of hand-editing
settings.json.

* fix(channels): correct shared instructions copy and pin multiline descriptor

The two tests this PR added pinned field key names only, so deleting
`multiline: true` from the shared descriptor left the CLI suite green
while `GET .../channel-types` served `instructions` as a plain string
field and the Web Shell rendered it through the single-line fallback.
Assert `kind` and `multiline` next to the key-list check.

The shared description also promised *extra* guidance for every
manageable channel, but dingtalk substitutes its own default block when
config.instructions is set (DingtalkAdapter.ts:908) instead of composing
like dws and github do. Reword to channel-neutral copy and pin the
qualifier so the additive promise cannot return silently.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmto8kxx1l2

* fix(web-shell): group channel instructions with conversation management

`instructions` joined the shared label map but neither section key set,
so the catch-all credentials filter swept it and the guidance textarea
rendered under "Credentials", after Client ID / Client Secret. It is a
clear-text field that gets none of the secret-kind handling, so add it
to SHARED_SESSION_FIELD_KEYS and let it render with the other shared
session controls. Also align the EN/ZH description with the corrected,
channel-neutral copy: dingtalk replaces its own default guidance when
this value is set.

Cover the multiline render branch, the only consumer of field.multiline
in the repo and the whole user-visible half of this change: a multiline
string field renders a TEXTAREA while a plain string field still renders
an INPUT, the control sits in the Conversation management section, and a
two-line value reaches onSave with the newline intact.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmto8kxx1l2

* test(channels): pin the shared instructions descriptor on every manageable built-in

The registry skips the shared `instructions` injection for any channel that
declares its own key, so the builtins suite only covered the synthetic
test-local plugin. Loop the manageable built-ins from the real catalog and
assert each serves exactly one `instructions` field carrying `kind: 'string'`,
`multiline: true` and the neutral copy. Without the render hint the editor
falls back to a single-line input, which flattens an operator's stored
multi-line guidance on the first edit.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtp6vibomd

* test(web-shell): pin the stored multiline instructions value in the editor dialog

All three dialog cases added for the multiline control rendered in create
mode, so the Textarea's display wiring was never exercised with a stored
value: replacing `value={String(value ?? '')}` with `value={''}` left the
whole suite green while an operator editing a configured channel saw an empty
Instructions box whose first keystroke replaced the stored guidance block.

Add an edit-mode case asserting the draft value reaches the textarea and
survives the save round trip. The fixture carries no outer whitespace because
createChannelEditorDraft loads untrimmed while assignField trims on save.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtp6vibomd

* i18n(web-shell): localize the channel noun in the zh instructions description

The zh value for channels.editor.field.shared.instructions.description was the
only Han-valued entry in the whole ZH map that still carried the raw English
noun, on a screen that renders it as the established spelling used by
channels.title (i18n.tsx:6484) and sidebar.channels (i18n.tsx:4902). Use that
spelling for both occurrences, and keep it distinct from
daemon.runtime.channel (i18n.tsx:4394), which names a different concept.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtp6vibomd

* fix(channels): scope multiline to string descriptors and pin the rendered copy

Two review findings on the shared multiline instructions field.

1. `multiline` was declared on `ChannelConfigFieldDescriptorBase`, which made it
   type-legal on every descriptor kind while its only reader honours it on
   `kind === 'string'`. Mirror the existing `envResolvable` shape instead: the
   attribute moves onto the string-carrying value descriptor, becomes
   `multiline?: never` on the plain-value, enum, number and object descriptors
   and on nested properties (which never reach a control at all - `renderField`
   returns null for `kind === 'object'`), identically in `channels/base` and in
   the SDK daemon mirror, with the wire-shape allowlist admitting the key only
   inside the existing string/secret guard.

2. The built-in-wide description pin in `channel-registry.test.ts` guarded a
   registry literal the Web Shell never renders: `fieldDescription` resolves
   `${labelKey}.description` and returns the i18n value whenever that key
   translates. Scope the copy pin to channels that do not declare their own
   `instructions` field (the registry skip branch), keep the two render
   invariants unconditional, and move the copy guarantee onto the surface an
   operator actually reads - one EN and one zh-CN assertion on the rendered
   textarea description, each matching text present only in that locale so the
   `messages[key] ?? EN[key] ?? key` fallback cannot mask a deleted key.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtpfg5ewmp

* docs(daemon): document the multiline channel descriptor modifier

This PR puts `multiline` on the daemon descriptor wire contract, but the
descriptor paragraph in qwen-serve-protocol.md only documented its sibling
client-interpreted modifiers `properties` and `exclusiveMinimum`. A third-party
daemon client implementing the channel editor from that page would render every
`kind: 'string'` field as a single-line input, and HTML value normalization
strips CR/LF on both parse and write-back. Since writes replace each field's
stored value wholesale, the first save of an unrelated field would persist the
flattened value.

Document `multiline` beside its siblings: it applies to string and secret
descriptors (both carry it in ChannelConfigValueFieldDescriptor), and the
descriptor types restrict it to top-level fields. Phrased as a type/contract
restriction rather than daemon enforcement, because assertManagementField
rejects misplaced envResolvable and exclusiveMinimum but has no multiline
branch. Also extends the existing preservation rule to cover a client that
renders such a field in a single-line control.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtpq5ybkn4

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-08 05:14:28 +00:00
jinye
965f498fd6
feat(cli): reap owned worktrees when daemon sessions are deleted (#11309)
* feat(cli): reap owned worktrees when daemon sessions are deleted

POST /sessions/delete removed the session record and sidecar but never
the owned checkout, marker, git registration, or branch, so deletes of
worktree-owning sessions leaked them permanently. deleteDaemonSessions
now classifies ownership before the record deletion (strict sidecar +
marker + containment + cross-sidecar sharing scan, executed under the
worktree ownership lock moved to a shared path-keyed module) and, after
a confirmed removal, safely deletes the checkout and branch — never
forcing the branch delete, preserving dirty checkouts, superseded
predecessors, tombstone-shared checkouts, and anything doubtful with a
named log line.

Refs #11024

* feat(cli): count never-committed files as work in the orphan cleanup gate

The cleanliness gate now runs a full git status --porcelain (untracked
files included, marker file exempted) so an agent-written draft that
was never committed preserves the checkout instead of being silently
destroyed. Also covers the invalid-marker branch and fixes the design
doc's logger reference.

* fix(cli): pin the destructive worktree cleanup to what the guards verified

- assert the removal target (originalCwd + slug) realpaths to the
  checkout every guard inspected, and consume the locked sidecar
- pin git status flags and scrub the environment in checkoutHasWork,
  exempting only disposable build output from the ignored tier
- refuse to fold the record on a session_closing refusal from a
  bridge-internal auto-close
- re-assert the workspace generation guard before the destructive step
- re-observe the checkout before logging preservation on removal
  failure, naming partial deletion when the shell is empty
- reject non-absolute or foreign sidecar bases and thread the runtime
  workspace cwd into the containment check
- keep no-sidecar deletes on the synchronous prefix so batch
  scheduling stays deterministic (gate-race flake)
2026-09-08 04:01:58 +00:00
易良
3ecfaffdf8
perf(dev): add pnpm worktree bootstrap foundation (#10449)
* perf(dev): add pnpm worktree bootstrap foundation

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(dev): harden worktree bootstrap process handling

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* perf(dev): prefer cache-only worktree installs

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(dev): preserve cached install cancellation

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(build): harden pnpm worktree bootstrap

Validate pnpm lock updates in releases and exercise real installs and builds across supported hosts. Preserve npm release compatibility and keep dependency-only setup from rewriting npm-layout notices.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* chore: record merge attribution

Record the required attribution for the Stage 1 merge without rewriting published history.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(dev): keep worktree bootstrap clean

Use a bootstrap-scoped notice guard because nested npm lifecycle commands replace npm_lifecycle_event. This preserves explicit notice generation while preventing dependency setup from rewriting tracked output.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* ci: track pnpm smoke workflow size

Register the new workflow in the repository size ratchet as required by the main CI gate.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(ci): drop stale importers from the new pnpm lockfile

The lockfile was generated before the WebShell cutover (#9811) removed
@qwen-code/webui from web-shell and the tailwind tooling plus
@qwen-code/webui from vscode-ide-companion, so frozen-lockfile
installs fail on all three smoke platforms with
ERR_PNPM_OUTDATED_LOCKFILE. Drop the five stale importer entries so
the lockfile matches the current package.json manifests; verified with
pnpm 11.24.0 install --frozen-lockfile --lockfile-only.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(ci): refresh pnpm lockfile after merging main

The merge of origin/main added remend@^1.3.1 to packages/cli/package.json
without updating pnpm-lock.yaml, breaking the pnpm Worktree Smoke workflow
frozen-lockfile install. Regenerated with pnpm install.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-conflict/jmtid3rse9h

* fix(scripts): resolve the path variable case-insensitively on Windows

A spread of process.env is an ordinary object, and native Windows shells
expose the path variable as `Path`, so `env.PATH` was undefined there and
findOnPath never located corepack — the offline-first Corepack bootstrap
silently degraded to npx on exactly the hosts it exists for.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(scripts): pin the worktree bootstrap guards against mutation

- Split the fallback log on /\r?\n/ like the sibling cmd.exe mocks, so the
  assertion holds on the Windows lane's CRLF output.
- Add a win32 variant bootstrapping with the native `Path` casing to pin
  the case-insensitive lookup.
- Assert the smoke workflow's fail-fast flag, the install-before-clean
  step order, and the no-build guard via a substring on the raw job text.
- Extend the pnpmfile rewrite fixture to devDependencies and
  optionalDependencies, which the committed lockfile already uses.
- Assert the notice-skip guard by effect (writeFile never called) and add
  the flag-absent companion test with I/O stubbed.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* chore(deps): sync pnpm-lock.yaml with qwen-live ACP backend deps

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-conflict/jmtjyz3tdco

* fix(dev): stop patch-package from corrupting the pnpm store

The root postinstall runs patch-package, which rewrites files under
node_modules in place. With pnpm's default import method those files are
hard links into the content-addressable store, so the patch rewrites the
store entry too and its contents stop matching the sha512 it is filed
under. Every subsequent fresh worktree then fails its --offline stage
with ERR_PNPM_NO_OFFLINE_TARBALL, silently falls back to the registry
(exit 0), and the fallback's postinstall corrupts the store again, so the
offline path never hits. The advertised offline timing was only reachable
on a same-tree reinstall, which needs no relinking.

Set packageImportMethod to clone-or-copy: a copy-on-write clone where the
filesystem supports it, a plain copy elsewhere. Either way patch-package
edits only the worktree's own copy and the store entry stays intact, so
later worktrees resolve entirely from the store.

Verified on an isolated fixture store: under the default method the
node_modules file and its store entry share an inode, and an in-place
edit moves the store file's sha512 off its address, after which a fresh
offline install fails with ERR_PNPM_NO_OFFLINE_TARBALL; under
clone-or-copy the same edit leaves the store hash unchanged and the fresh
offline install reports reused 1, downloaded 0.

Also add pnpm-lock.yaml to .prettierignore. prettier reflows it into a
shape pnpm does not emit, so `npm run format` and `pnpm install` fight
over the file; .yamllint.yml already ignores it for the same reason.

Both facts are pinned by tests in scripts/tests/package-scripts.test.js.

Claude-Session: https://claude.ai/code/session_012797rgiteWJxLT9TLkKq8G

* fix(ci): refresh pnpm-lock.yaml for qwen-live prompts deps

Main added prompts/@types/prompts (and the ACP sdk) to
packages/qwen-live/package.json after this branch generated its
lockfile, so the pnpm Worktree Smoke frozen-lockfile install failed
with ERR_PNPM_OUTDATED_LOCKFILE. Regenerated with pnpm 11.24.0
(--lockfile-only); frozen-lockfile and supply-chain policy checks pass.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-conflict/jmtlc9sozfg

* fix(pnpm): close review findings on worktree bootstrap

- Declare @qwen-code/qwen-code-core in vscode-ide-companion so the pnpm
  linker materializes it; regenerate both lockfiles and assert the link in
  the smoke workflow (R5-1)
- Pin @types/node 20.19.1 in packages/core so the pnpm layout compiles
  against the same types npm hoists (D6-1)
- Accept and pin the corepack +sha512 integrity suffix on packageManager;
  strip it for the npx fallback spec (D6-2)
- Cover every workspace member in the pnpmfile rewrite set, mirror the npm
  channel list in pnpm-workspace.yaml, and cross-check both in tests
- Run setup-worktree.js installs with cwd pinned to the checkout, gate the
  smoke clean-check on git status --porcelain, stop post-merge runs from
  cancelling each other, and extend check-lockfile.js to pnpm-lock.yaml

* merge(main): realign pnpm workspace set and lockfiles

Main removed the webui and cua-driver packages; drop both from the pnpm
rewrite set and regenerate the pnpm lockfile against the merged manifests
(picks up playwright, react-markdown, and the other main-side dependency
changes) so the frozen bootstrap matches what the PR merge ref will run.

* style(ci): quote smoke workflow scalars per yamllint

* fix(dev): require Corepack for pnpm bootstrap

* fix(dev): close pnpm bootstrap review blockers

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: yiliang114 <jinjing.zzj@gmail.com>
2026-09-08 03:07:40 +00:00
ytahdn
663e55bd29
feat(web-shell): refine file cards and configure drop destination (#11298)
* feat(web-shell): customize artifact icons

* fix(web-shell): preserve artifact icon fallbacks

* fix(web-shell): improve artifact fallbacks

* feat(web-shell): refine file cards and configure drop destination

* fix(web-shell): address file card and drop review feedback

---------

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
2026-09-08 02:56:51 +00:00
Shaojin Wen
96b14f2311
feat(web-shell): add a context usage tab to the right sidebar (#11177)
* feat(web-shell): add a context usage tab to the right sidebar

Surface current context-window composition beside the existing token
consumption tab. The new opt-in header action opens a session-owned
context_usage artifact tab that reuses the /context renderer in a compact
sidebar layout, with manual refresh and owner-bound restoration.

The data path already exists (getContextUsage with detail), so this adds no
endpoint or core change. Tabs deduplicate, persist, restore lazily against
their saved session, and close with their pane or session, mirroring the
token tab lifecycle. Transcript /context behavior is unchanged.

* fix(web-shell): address context tab review round 1

- Share one transient-read predicate between the two usage panels so both
  swallow disconnect, transport-close and fetch-failed identically.
- Keep the last good context reading during and after a failed refresh;
  only owner changes clear it.
- Dedupe the mount collection across StrictMode replays with an owner-keyed
  in-flight request instead of a closure-local flag.
- Reclaim pane-bound usage tabs at restore completion; the mount-time pane
  cleanup runs before restoration lands, so they survived reloads into a
  non-split view.
- Let the compact panel opt out of the 30-char detail-name cap, render the
  progress meter as proportional blocks, uncapped meta row and indented
  sub-rows, and align the zh-CN wording with the established term.
- Pin the gaps the review measured: transcript-insertion negatives, pane
  action owner binding, primary-session restore arm, standalone opt-in,
  no-actions render, and the compact/transcript truncation contract.

* fix(web-shell): address context tab review round 2

- Reclaim pane-bound usage tabs from an effect gated on a new
  splitViewSettled flag instead of at restore-commit time: mainView and
  splitSessionIds are still their useState initials when the restore
  continuation runs, so deciding there dropped legitimately pane-bound tabs
  and wrote the drop back to storage.
- Rethrow transient getContextUsage failures without a notice, mirroring
  getStats: the context panel re-collects on every activation, so the notice
  channel stacked identical errors while a session was down.
- Emit the compact meter segments in legend order (used, free, buffer) and
  pin widths and threshold colors in tests.
- Pin the alert-over-retained-reading precedence, the remaining reclaim
  filter quadrants, and the reachable pane-bound restore shape; type the
  pane context fixture and build the pane actions mock as an override of the
  main one so it satisfies the declared contract.

* fix(web-shell): address context tab review round 3

- Settle the split latch at every bootstrap exit, including superseded
  classifications and the non-workspace bail, and gate the non-split close
  twin on it so a live pane tab is never dropped mid-decision.
- Re-apply the pane-bound predicate at restore commit once the latch is
  already true, so a stale tab cannot be selected active and mount a foreign
  collection; defer to the settled sweep while the decision is pending, and
  re-run the sweep when the restored tab list lands.
- Make getContextUsage silence opt-in (silent) so the auto-recollecting panel
  stops stacking notices while user-initiated callers keep the attributed
  notice and suppressed duplicate toast.
- Pin the gaps the round measured: uncontrolled split bootstrap paths,
  superseded-classification settle, language-change survival, closed-panel
  restore, serialization of closeWithPane, binding replacement on re-open,
  render assertions for restored payloads, aria-busy during background
  refetch, clearing of unusable refresh results, transcript glyph math,
  meter-legend agreement, and the widened transient predicate at call sites.

* fix(web-shell): address context tab review round 4

- Re-arm the split latch when a classification actually starts, in both
  openSplitView and the controlled split effect, so a later decision re-opens
  the reclaim window instead of being licensed by an earlier settled one.
- Close the right panel when the restore-commit reclaim empties the tab list,
  mirroring closeArtifactPanelTabs, so an emptied panel is not persisted open.
- Dedupe silent hard context-usage failures through the existing
  silentHardFailureNoticeKeys pair, matching getTasks/getWorkflowTasks.
- Delete the non-split close twin subsumed by the settled sweep, which also
  removes its missing artifactPanelRestoring guard.
- Pin the re-arm, the do-not-settle branch under overlapping classifications,
  the emptied-panel close, the deep-link storage purge with a positive
  control, and the silent hard-failure notice semantics.

* fix(web-shell): address context tab review round 5

- Settle the split latch on a terminal capabilities error in the controlled
  split effect, so a capabilities blip cannot strand the latch unsettled
  while no classification can start; a merely pending load still defers.
- Route silent context-usage hard failures through noticeForSession and gate
  dedupe-key registration on the session still being live, so the registry
  stays session-scoped and a stale in-flight failure neither toasts for a
  session the user left nor suppresses the notice of the session on screen.
- Extract resetEmptyArtifactPanel and share it between closeArtifactPanelTabs
  and the reclaim-emptied commit path so the empty-panel reset cannot drift.
- Pin the owning-classification reclaim half of the overlapping-classification
  test, the capabilities-error settle, the registry teardown clear, and the
  per-call non-silent notice.

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-08 01:22:42 +00:00
jinye
f1ed3bc31a
feat(external-context): Add daemon memory writes (#11311)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Lint & Static (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / web-shell E2E Smoke (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (no-AK, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (CLI, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Desktop Shell (ubuntu-22.04) (push) Blocked by required conditions
Qwen Code CI / Desktop Shell (windows-2022) (push) Blocked by required conditions
E2E Tests / Build for E2E (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 1/2 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 2/2 (push) Blocked by required conditions
E2E Tests / E2E Interactive - OpenTUI renderer (bun) (push) Blocked by required conditions
E2E Tests / channel-plugin E2E (nightly) (push) Blocked by required conditions
E2E Tests / cron-interactive E2E (nightly) (push) Blocked by required conditions
E2E Tests / web-shell Browser Regression (push) Waiting to run
npm cache producer / Save npm cache (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
Add an opt-in workspace-bound writer and complete literal MCP approval previews.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-07 16:33:50 +00:00
qqqys
bc13d167df
fix(goal-draft): keep drafts concise and verifiable (#11284)
* fix(goal-draft): keep drafts concise and verifiable

* fix(core): align goal-draft gate and budget default with the documented contract

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-09-07 16:27:18 +00:00
jinye
a4a62258b1
feat(web-shell): add continuous history and compact turn navigation (#11208)
* feat(web-shell): add bounded historical transcript viewport

* fix(web-shell): preserve historical viewport reading state

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(web-shell): handle pending history entry cancellation

Preserve live scrolling when cancelling an in-flight history entry and show retry feedback for a stale boundary without reporting errors for abandoned intents or sessions.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(web-shell): cover historical viewport cache guards

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* feat(web-shell): restore continuous scrolling with compact turn navigation

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* docs(web-shell): sync Phase 3 delivery and verification status

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: jinye.djy <jinye.djy@U-Y37CF7G3-1943.local>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-07 15:52:37 +00:00
jinye
55926e4617
feat(serve): allow concurrent standalone daemons with session fencing (#11207)
* feat(serve): allow concurrent standalone daemons with session fencing

* fix(serve): address standalone ownership review feedback

Preserve writer-blocked navigation and queued drafts, retain Live admission diagnostics, and cover concurrent daemon and filesystem ownership boundaries.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(serve): handle all writer fences and legacy publishers

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(ci): sync standalone concurrency allowlist assertion

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: jinye <3007091+doudouOUC@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-07 15:52:05 +00:00
jinye
c526bb80b1
feat(channels): Implement named-session worktree reset (Part 4B) (#11015)
* docs(channels): Add named sessions Part 4B design

Propose same-worktree conversation replacement for named worktree
tasks: a compensatable daemon-side transfer that resets the selected
task's conversation while retaining its exact verified worktree.

Also absorbs the findings deferred from #10643: missing-marker
recovery via reset, deferred-prompt restore attestation, and
orphan-reap worktree cleanup bounded by unambiguous ownership and a
clean checkout.

Refs #10103

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* docs(channels): address self-review of Part 4B design

- Idempotent resume short-circuits the marker transfer when the marker
  already names the replacement, so a post-rename crash retries as a
  no-op; the resume check also requires the replacement's supersedes
  back-link.
- Manager reset preserves only the creation timestamp; the update and
  selection timestamps bump as they do on a shared reset.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* docs(channels): revise Part 4B design after review

Address the design review on #11015:

- Correct the deferred-prompt premise: the under-attesting restore
  branch serves a genuinely live prompt, so it is removed to fail
  closed instead of attempting an impossible relocation (C1/S5a).
- Surface parked deferred restore prompts in the bridge's active state
  and consume it in both the coalesced restore (R8-2) and the reset
  quiescence check (C2); add a reset-pending admission barrier and a
  pre-flip re-verify so quiescence is enforced, not sampled (M3).
- Make the marker transfer a compare-and-swap: owner re-check at the
  rename commit via atomicWriteFile's assertCanCommit, O_EXCL create
  for the missing-marker hatch, and worktree-keyed serialization (C3,
  S1, S2).
- Move the orphan-reap worktree cleanup and the R8-1/R8-3/R1-1/F3
  fixes out of this part; record every standing Part 4A finding's
  disposition in a dedicated section (S4, M1, M2).
- Add the interrupted-transfer typed signal and message so the one
  retry-repaired window tells the user to retry.
- Credit the review bot's R3-2 alongside the human findings.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* docs(channels): address round-2 review of Part 4B design

Scope the R8-2 deferred-prompt visibility fix to the coalesced-restore
waiter and getSessionSummary, and state that it must not feed the restore
response's hasActivePrompt. Widening it there would push a deferred
worktree restore into the active-session check this revision now relies
on for the removed branch, failing closed a path that works today.

Constrain the marker transfer to atomicWriteFile's rename commit: refuse
a marker owned by a foreign uid so the ownership-preserving in-place
write is never selected, and record that the sibling temp file keeps the
EXDEV unlink-then-create fallback unreachable. Both paths would otherwise
break the "never a partial file" guarantee, and a truncated marker reads
as invalid, which is never auto-recovered.

Use a new synchronous no-follow strict marker reader for the
assertCanCommit re-check, since the hook is synchronous and cannot await
the async reader; atomicFileWrite.ts stays unchanged.

Also record that the reset-pending barrier is deliberately non-durable,
and note why the R8-1 rollback block differs from the two neighbouring
handlers that preserve on an inconclusive delete.

* docs(channels): link Part 4B dispositions to follow-up issue #11024

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(channels): resolve residual worktree session defects from #10643

Four live defects in main, none depending on the Part 4B transfer
protocol, fixed with regression tests:

- R8-1: the post-spawn generation-check failure block now removes the
  unowned worktree regardless of the orphan-delete outcome. Unlike the
  relocation/persistence handlers, no session has entered the checkout
  at that point, so an inconclusive cleanup cannot strand it.
- R8-3: reattach heals a stale persisted-v1 claim when the daemon
  resumes with no worktree object (legitimate exit), while a response
  still carrying worktree metadata keeps failing closed.
- R1-1: createWorktreeSessionMarkerExclusive unlinks the file it
  created when the write fails, so the path no longer wedges on EEXIST.
- F3: /session new --worktree without a name returns the usage line
  instead of a misleading name-validation error.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* docs(channels): address round-3 review of Part 4B design

- Make the foreign-uid marker refusal conditional on the platform
  reporting an effective uid (process.geteuid), so Windows — where the
  ownership-preserving path is unreachable by construction — refuses
  nothing; both strict marker readers surface the owner uid.
- Bound the no-partial-marker claim honestly: the commit-point re-check
  compares owner and file identity (dev/ino/uid); the residual
  in-place-rewrite case requires a same-uid writer, which the
  worktree-keyed lock excludes for daemon writers.
- Name the reset-pending barrier's refusal shape (the typed
  worktree_reset_active failure) and record that Channel callers never
  observe it because the owner lock serializes the whole reset.
- Point the four residual fixes at this PR (code and tests included)
  and reference follow-up issue #11024.

* fix(channels): harden marker cleanup and parser guard per round-4 review

- The failed-create cleanup unlinks only while lstat still resolves to
  the inode this call created, so the identity-changed branch no longer
  deletes a foreign file swapped in during the write window (verified by
  a new swap-injection test).
- The success-path close() moved past the catch: a close rejection now
  propagates with the fully written, fsync'd marker intact instead of
  unlinking it — required by the Part 4B missing-shape commit, whose
  rollback never removes the worktree.
- The /session new guard covers any leading-flag token
  (parts[0]?.startsWith('-')), not just --worktree; no legal task name
  starts with '-', so the class is closed without swallowing names.
- The reattach heal comment states what the client can actually observe
  (no worktree object may also mean a cleared in-memory association or a
  provenance that skips sidecar restore) instead of an exhaustive
  inference.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* docs(channels): address round-4/5 review of Part 4B design

- Critical: the resume path reads the marker before choosing its
  recovery shape — a committed transfer (marker names S_new) is never
  rolled back; a busy replacement fails closed with
  worktree_reset_active and converges on a later retry; the destructive
  rollback is confined to the pre-commit shape where S_new is provably
  non-authoritative. Protocol steps 4-5 and the verification plan
  updated to match.
- The no-deletion contract is narrowed in all four places to
  user-visible/pre-existing state, with rollback permitted to remove
  only the replacement session's own sidecar and record.
- Non-goals inverted: the four residual fixes land in this PR; only the
  reap cleanup, sibling delete path, and SessionRouter.ts:487
  investigation stay out (tracked in #11024).

* feat(channels): reset worktree tasks by transferring checkout ownership

Implement the Part 4B ownership-transfer protocol so /clear, /new and
/reset work on worktree-isolated named tasks: the task keeps its
worktree, files and branch, and gets a fresh conversation.

Daemon: POST /session/:id/worktree-reset spawns a replacement in the
root workspace, relocates it into the verified checkout, links the
sidecar pair (supersedes / supersededBy), flips the ownership marker
last, then attests persisted-v1 and severs the superseded session.
Preconditions fail closed with a bounded typed 409 taxonomy
(worktree_reset_unsupported / _active / _invalid_state); a prompt
barrier fences the superseded session for the transfer's duration.
Crash recovery is marker-driven: pre-commit shapes roll back and
complete a fresh transfer in the same request, committed shapes resume
idempotently. Restore gains the superseded redirect
(worktree_session_superseded + replacementSessionId), the typed
missing-marker signal, and the interrupted-transfer classification;
the under-attested unlocated-restore branch is removed. Worktree
restore/reset serialization is re-keyed from session id to canonical
worktree path; its promise-chain release logic is byte-identical to
main, so this claims no fix there, and the refused-transfer release
test now pins that the lock is released.

Core: transferWorktreeSessionMarkerOwner commits through atomicWriteFile
with a synchronous strict re-read at the rename commit point, refuses
foreign-uid markers where ownership is observable, and recreates a
missing marker through the O_EXCL hatch; both strict readers surface
uid/dev/ino; the transfer temp sibling is excluded from git.

Bridge: a parked deferred restore prompt now counts as active in
getSessionSummary and the coalesced-restore waiter (never in the
restore response, which the route reads); new reset-pending barrier
plus clearSessionWorktree / severSessionClients surfaces.

Channels/SDK: DaemonSessionClient.resetWorktree, capability-gated
DaemonChannelBridge.resetWorktreeSession, SessionRouter transfer +
superseded redirect with registry self-heal, manager reset with the
busy refusal and one automatic interrupted retry, and the /clear
message surface. Also logs a failed worktree removal in the R8-1
generation-closed rollback instead of swallowing it.

* docs(channels): correct the R8-1 rollback rationale in the Part 4B design

The disposition paragraph explained the fixed block with the
spawn-failed outer catch's rationale ("under !spawnCompleted no session
exists"), but the block this PR changes is the post-spawn
generation-guard catch, where the session exists and orphan removal is
attempted. Its actual rationale is that relocation has not been
attempted yet, so no session has entered the worktree and nothing can
own the checkout. An implementer reading the normative disposition
record would otherwise carry the wrong invariant into the reset
rollback design.

* fix(channels): register loop tools for a worktree reset replacement

resetWorktreeSession attached the replacement session without the
channel-loop MCP reconciliation that newSession and loadSession both
run, so a task reset through the ownership transfer silently lost its
channel_loop tools until the next handler registration sweep. Mirror
the loadSession shape, including the enableChannelLoops opt-out.

* docs: align worktree-reset error taxonomy names with the implementation

The SDK doc comment and the protocol reference still carried the
pre-implementation code name worktree_reset_not_worktree_session and
described corrupt-state failures as untyped 500s. The route ships the
design's taxonomy: worktree_reset_unsupported and a bounded,
non-destructive worktree_reset_invalid_state 409 whose specific reason
goes to the daemon log.

* fix(cli): register the worktree-reset route and tag in the contract surfaces

Three drift guards caught the new surface that targeted test runs
missed: the telemetry route catalog now lists POST
/session/:id/worktree-reset (and its drift-guard count moves to 66),
the integration baseline capabilities list gains
session_worktree_reset_v1, and the daemon index capability count moves
to the registry's actual 154 (the doc was already one behind before
this tag landed).

* fix(cli): update the telemetry route catalog audit counts for worktree-reset

The second drift guard (telemetry.test.ts) audits the catalog's size and
attribution split; the new POST /session/:id/worktree-reset entry moves
them from 65 routes (63/2) to 66 (64/2).

* fix(channels): address round-3 review of the worktree reset implementation

Fifteen criticals from the round-3 review pass, plus its documentation
accuracy findings and the two defects those exposed.

Transfer protocol:
- resume-path link agreement no longer consults the replacement's transcript
  record, so a cold-start committed transfer resumes instead of being refused
  on every retry
- an absent marker reads as pre-commit only while the replacement is dormant;
  a live replacement is the committed owner and is refused before any
  destructive write, rather than dismantled and double-written
- the worktree-keyed ownership lock covers every non-live Part 4A restore, not
  just Channel-sourced ones, so a source-less restore cannot attest exclusive
  ownership while a transfer moves it
- severing a superseded session issues one detach per remaining registration
  (they are refcounted) and bails when a pass makes no net progress, so a
  reconnecting client cannot spin a request that holds the ownership lock
- the interrupted 409 no longer carries a replacementSessionId that named the
  caller's own session

Channel bookkeeping:
- a failed reset evicts the old id from the live set, without which the
  superseded redirect can never heal a post-flip failure
- healed replacement ids survive close()'s rollback path, resumeReserved's
  dispatch, and every registry write
- the two restore-surface recovery messages moved to the shared named-session
  error surface, which is where their producers actually emit; the manager's
  retry loop around codes the reset route never returns is gone

Readers: an interactive --resume refuses a sidecar naming a replacement and
preserves it as recovery evidence; a parked deferred restore prompt now also
reads active on the daemon status snapshot.

Docs and SDK docstrings corrected to the shipped contract: no caller
registration on the reset response, marker-missing repair is a reset rather
than a restore retry, invalid_state is non-destructive only for what the
request itself started, the design names the renamed ownership lock and puts
the superseded redirect where the code resolves it, and the reset route's dead
clientId parameter is removed.

* test: pin the worktree transfer invariants round 3 left uncovered

Three of the round-3 coverage findings guard invariants the whole transfer
protocol rests on, so they are worth their tests rather than a follow-up:

- the marker transfer's two commit-window guards: a marker owned by a
  different uid is refused before anything is staged, and a marker swapped
  between the opening read and the rename aborts the commit instead of
  clobbering the racer
- the resume path's link-agreement refusals on both sides of the flip: a
  missing replacement sidecar pre-commit, and a replacement that disowns the
  old session post-commit, each fail closed and leave the interrupted state
  untouched for repair
- a failure after the marker flip never compensates backwards: the sidecar
  links stay, the replacement is not orphan-reaped, and the marker still
  names it

Test-only; no production code changed. Each was mutation-verified by removing
the guard it pins and confirming the new test goes red.

* fix: address round-4 review of the worktree reset transfer

Fifteen criticals from the round-4 review, verified against the code before
changing anything. The sixteenth was a false claim in the PR description
rather than a defect, and the description is corrected separately.

Transfer protocol:
- the old sidecar's backward link is durable before the replacement's forward
  link, so every pre-flip crash window lands on a shape the resume
  classification can observe instead of stranding a replacement no retry sees
- the superseded redirect is decided from a re-read taken under the ownership
  lock, not from the pre-lock read that only supplies the lock key
- the marker primitive distinguishes committed from not-committed across its
  own close tail, so a committed flip whose close rejected is never rolled
  back destructively
- severing reports whether the old session is actually gone; a survivor keeps
  the barrier armed and is reported on the wire instead of being papered over
  by a 200

Fence and ownership:
- the reset barrier covers every writer that reaches the checkout or the
  session cwd, not just prompts
- the stale-marker hatch in exit_worktree no longer reads a retained
  superseded sidecar as ownership of the replacement's checkout
- detaching a managed session releases it regardless of the live flag a
  failed reset drops

Channel bookkeeping:
- a queued turn bound to a superseded id follows its task onto the
  replacement, so later turns of the same chat are not silently dropped
- worktree recovery advice names the task and recommends clearing only when
  the broken task is the selected one, so it cannot direct a transfer at a
  healthy task
- a non-Channel cold restore whose prompt the bridge fired keeps its worktree
  instead of failing closed and killing the session

Contract text narrowed to what the code decides: the superseded redirect is a
link-driven hint to verify rather than proof of ownership, crash safety is
stated per window, the reattach heal does not gate the retry it performs in
the same call, the SDK's self-heal claim enumerates the shapes that heal and
says the rest do not converge, and the reset response is documented as
carrying a minted client registration on a fresh transfer.

* fix: address round-5 review of the worktree reset transfer

The ten Critical findings from round 5, each verified against the code
before changing it. Nine are fixed here; the tenth is a false claim in an
earlier commit message and is corrected in that message.

Route: the reset barrier now survives the marker flip. Once ownership has
committed, releasing the barrier stops being this request's to do, so a
post-commit failure leaves the superseded session fenced for the retry
that finishes the severance instead of re-admitting a writer to a checkout
the marker already handed to the replacement. Both rollbacks confirm the
interrupted replacement's removal before unwriting the links, and refuse
when it survives rather than spawning a second writer beside it with
nothing on disk naming it. The resume rollback unwrites the forward link
first, matching the fresh path's compensation, so a crash mid-rollback
leaves the backward link a retry refuses for repair instead of a forward
link nothing scans for. The route also stops at a disconnected caller
before the flip and lets the pre-commit rollback reap the replacement;
`res.writable` is an own data property that stays true after a disconnect,
so the guard reads `res.destroyed` and `res.socket.writable`.

Channels: a superseded-redirect heal that cannot persist the registry no
longer rewrites a successful load into a reported load failure whose
message asserts the opposite of what happened. A pre-flip reset failure
records the surviving bridge binding so the recovering load releases it
before attaching, instead of being aborted by the session-replaced
notification its own attach would emit.

Bridge: the workflow-task and goal-control writers join the barrier. Both
start work in the session cwd without passing prompt admission, and
neither needs a client registration to be reached.

Core: the missing-marker hatch stages an fsynced sibling and publishes it
with link(2), so the marker path never exists without complete content and
the exclusive create leaves no 0-byte window. The publishing link is the
compare-and-swap against absence, so exclusivity is unchanged; the hatch
now requires hardlink support and fails closed where it is unavailable.

Docs: the restore prose names the cold-restore branch that still returns
unattested worktree metadata instead of claiming an unqualified fail-closed
rule, the barrier prose enumerates the eight fenced writers and says plainly
that the fence is not everything that reaches the child, and the marker
mechanism, barrier-release and rollback-order descriptions match the code
above.

* docs: mark supersededSessionLive as diagnostic-only

R5-11 and the maintainer's real-daemon verification both note that the
field is written on two response paths and typed in the SDK, while no
first-party caller reads it. Say so where a reader looks for the
contract instead of leaving the SDK comment stating an obligation
nothing honours: what keeps a surviving superseded session out of the
checkout is the armed barrier plus the on-disk sidecar link, not this
flag, and the channel worker reports the same success message either
way. External callers that need to tell the two cases apart can still
read it.

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-09-07 11:54:15 +00:00
qqqys
50b33052cd
feat(goal): show what a Goal has spent against the window it is allowed (#11248)
* feat(goal): show what a Goal has spent against the window it is allowed

A Goal's autonomous spend window is enforced but invisible. `tokensUsed` and
`tokenBudget` have been on the record since budgets landed, the 30M default
stops the Goal and hands it back to the user, and nothing anywhere shows how
close it is to that. A `tokensUsed` grep across the TUI and Web Shell returned
only test fixtures. The user watched the turn count climb with no way to tell
a Goal two percent through its window from one about to stop.

The footer pill, the ink status card, its OpenTUI counterpart, and headless
TEXT status now report the spend. The pill and the cards abbreviate
(`1.2k/30.0m`) through the existing `formatTokenCount`; TEXT spells the
figures out, since that output is read in scrollback and piped into scripts,
neither of which is helped by `1.2k`.

Three display rules, the same at every site. A Goal that has not billed a turn
shows nothing: a fresh `0/30.0m` says nothing the status has not already said,
and the pill has little room to say it. A Goal with no budget shows what it
spent, which is the whole of what is known. A stopped Goal keeps showing its
spend, because what a paused or blocked Goal cost is exactly what the user is
deciding about.

The OpenTUI card computes its own subtitle from its own snapshot type, so both
were extended alongside the ink card rather than left to drift apart under the
parity gate. `formatGoalState` is exported to be pinned directly: the states
worth checking are far cheaper to construct as snapshots than to drive a
headless run into.

The figure is the Goal meter's own scope -- the model calls a Goal makes in
its own turns, excluding subagents and the verifier's checks -- and the user
doc says so.

* docs(goal): say that resuming a spent Goal adds a window rather than resetting

Resuming sets the budget to what the Goal has spent plus another grant,
so the pill reads 30.0m/60.0m straight after a resume. The paragraph
said the window is what the setting says, which holds only for the
first one.

Also narrow the no-budget card assertion, which read as "no slash
anywhere in the frame" and would have failed on a slash in the
objective.
2026-09-07 11:01:34 +00:00
jinye
be9d5e8d3f
feat(external-context): Add opt-in auto recall for administrator-owned Mem0 dialects (#11246)
* feat(external-context): add opt-in Mem0 auto recall

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(external-context): bound auto recall process lifetime and sanitization

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(external-context): separate hook test startup and execution budgets

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-07 10:36:28 +00:00