Commit graph

423 commits

Author SHA1 Message Date
易良
eeec911c20
fix(ci): raise the build heap cap from 4096 to 6144 MB (#14822)
The packages/cli TypeScript build inside the Desktop Checks
'Prepare bundled runtime' step now dies with 'Ineffective
mark-compacts near heap limit' at exactly 4030 MB on both macOS
jobs, i.e. it hits the 4096 MB ceiling the root build script sets
via NODE_OPTIONS; the Linux and Windows legs of the same run finish
under the cap (#14793). The CLI project graph crossing the 4 GiB
type-checking threshold leaves neither macOS leg headroom.

Raise the build-time ceiling to 6144 MB, mirroring #11781's earlier
3072 -> 4096 bump. It is still a ceiling: V8 only consumes what a
build actually needs, so local builds and runners whose jobs fit
under 4 GiB are unaffected.
2026-10-20 02:55:35 +00:00
易良
5ac26809fe
fix(ci): include updatedInput hook integration tests in PR checks (#14747)
* fix(ci): include updatedInput integration coverage in PR checks

* test(scripts): pin that every no-AK entry still resolves to a test file

Vitest positional args are filename filters, not paths: a renamed entry
matches nothing while the other filters keep vitest exiting 0, so the gate
would report pass with that suite's coverage silently gone. Assert every
pinned entry still exists under integration-tests/.

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-10-20 10:39:41 +08:00
qqqys
de20ab6a30
feat(workflow): authorize daemon ancestor workflows (#14646)
* feat(workflow): authorize daemon ancestor workflows

* fix(workflows): fence ancestor trust across policy and cwd changes

Preserve the actual admission publication through reconciler rebuilds.
Reject in-flight and stale-directory replies throughout physical cwd changes, including partial failures.

Refs #14646

* fix(workflows): retain ancestor trust after context clear

* fix(workflows): normalize restored ancestor trust session ID

* test(workflows): use native cwd for restore trust fixture

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
2026-10-19 15:00:52 +00:00
Shaojin Wen
c076c699af
test(managed-agent): add G3 approval fault-point E2E arms (#14687)
* test(managed-agent): add G3 approval fault-point E2E arms

Step 3's packaged gates crossed the approval path without a fault. Four new
arms layer one failure on the Allow arm: a crash after the decision commits,
a crash after the approved execution reaches PREPARED behind a held Runtime
Broker start, a lost decision reply retried under the same Idempotency-Key,
and a lost Turn admission reply retried the same way.

Part of #14481 (step 2).

* test(managed-agent): gate the approval crash on the committed decision

The response endpoint's 202 only admits the response operation; the Action
row stays requested until delivery commits. The after-decision arm now holds
the Runtime Broker start and waits for state=decided with a decision receipt
and digest before SIGKILL, and a predicate test rejects the admitted-only
answer.

Also add the four fault arms to the hosted-process-ci inventory and raise
its step/ceiling expectations to 190/205.

* fix(managed-agent): scope the approval decision barrier query

The after-decision arm's durable-decision wait referenced actionFilter, a const inside the earlier approval-setup block, so the new arm threw ReferenceError before it could crash the Harness. Build the same predicate from the in-scope sessionFilter and the record/action columns instead.

---------

Co-authored-by: 易良 <1204183885@qq.com>
2026-10-19 14:22:44 +00:00
qqqys
c4f0acdef8
feat(extensions): add bounded generated HTML and JavaScript views (#14338)
Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
2026-10-19 13:09:33 +00:00
Bob.qwencode
530a22680d
chore(release): v0.25.3 [skip ci]
* chore(release): v0.25.3

* docs(changelog): sync for v0.25.3

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-19 07:38:12 +00:00
qqqys
bd63f900c7
feat(workflow): discover trusted ancestor workflows in local sessions (#14369)
* feat(workflow): discover trusted ancestor workflows in local sessions

* fix(workflow): enforce ancestor trust authority and preserve scoped loading

* test(workflow): use discovery fixture agent type for initialization

* test(workflow): support partial settings fixtures in ACP trust mock

* test(acp): provide operator trust view in worktree fixtures

* fix(workflow): preserve safe discovery on root I/O errors

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
2026-10-18 19:54:22 +08:00
Bob.qwencode
25ef506c03
chore(release): sdk-typescript v0.1.20 [skip ci]
* chore(release): v0.25.2

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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-15 17:01:33 +00:00
Shaojin Wen
95468e293f
test(managed-agent): gate G3 Step 3 recovery, model-round takeover and control-plane fencing (#14181)
* test(managed-agent): gate G3 Step 3 recovery, model-round takeover and control-plane fencing

Run the five Step 3 packaged gates (approval Allow, Deny, Cancel and
expiry, and the continuation closeout) as hosted-harness-mysql steps
through a new --g3-verify flag; G3_VERIFY_MODE keeps working.

Add a model-round arm: the first model round of a Session with no
Workspace survives two Harness-only replacements, each reissuing the
same saved request and retracting the dead owner's prefix, with no tool
execution and a clean next-Turn history.

Add a frozen control-plane arm for the binding half of #12952 Q2: the
original Spring and Harness are SIGSTOPped mid-round, a second pair on
the same MySQL finishes the Turn, and after SIGCONT the woken Spring's
stale claim, witnessed in the MySQL general log, must leave the Session
binding, the Turn row, the public events and the journal unchanged.
The woken coordinator never reaches a bind call, so a store test pins
the bind guards directly: a former owner whose claim lapsed cannot
rebind, renew or admit, and a terminal Turn cannot be bound.

Fix the server README paragraph that still said a model-round park
wedges the Session, document every G3 arm and the first-release
recovery scope, and record decision D11 in both languages of the G3
design.

Refs #12952

* test(managed-agent): answer the first review of the G3 residual gates

The README no longer says a parked model round or approval in a Session
with Hooks or MCP ends with a typed decline: a Hooks Session never takes
over and an MCP approval plain-attaches. It now says Step 3 neither
reissues nor resumes those states and no gate covers how they end.

The model-round arm requires the writer generation, journal revision and
committed sequence to advance across every takeover, and its terminal
wait watches both the live control plane and the Harness finishing the
round; waitUntil and waitForTerminal accept several children for this.

The frozen control-plane arm gains a writer-side witness: the woken
Harness must meet the store's stale-writer refusal after the wake and
not before, counted on its output since the wake rather than the
rolling log tail. It also refuses to wake a Harness whose home is gone,
reports the retained home, and splits a poll budget across the
snapshot's synchronous probes while one-shot snapshots keep 10 s each.

The store test pins the owner check of recordRecoveryAdmission for a
former owner, and both admission refusals now check their message.
New pins cover the writer side of the Step 3 evidence path, the awaited
waits, the pre-wake baseline and the checks above. The design doc's
stale CI budget item now points at D11, which states the step sum.

Refs #12952

* test(managed-agent): answer the second review of the G3 residual gates

waitUntil's deadline message now takes its log tails from the normalised
child list, so a deadline on the model-round terminal wait reports both
tails instead of throwing a TypeError on the array. An early exit names
the child that died, which tells the control plane apart from the Harness
generation finishing the round.

The waitUntil tests cover a dead first child, a dead last child and a
deadline with both children alive, and the signal-death comment sits on
its own test again.

The frozen control-plane arm watches the replacement Spring and Harness
through the quiet window and every wait after the wake, and checks them
once more before the fenced verdict: a dead owner would otherwise hold
every snapshot still.

The README and both design docs no longer say the new gates run in CI.
They are steps of the Hosted MySQL job, which runs daily or on demand and
reaches them only after its verification step passes; that step was
failing on main when the gates were added. The D11 snapshot list now
says the journal probe is a per-generation, per-operation count and
highest revision.

Refs #12952

* test(managed-agent): let the frozen arm see an owner exit during its probes

The fenced snapshot's probes are synchronous, so a replacement that exits
while they run is not recorded until the event loop turns. The arm now
yields once before checking the owners, and the pin covers the check's
body, not only its position.

The docs say the new steps run only when every earlier step of the job
passes, not only the Verify step, and state in the past tense that no CI
run had executed them when they were added. The README's no-change
statement is bounded by what the D11 snapshot can see.

Refs #12952

---------

Co-authored-by: 易良 <1204183885@qq.com>
2026-10-15 19:29:48 +08:00
ytahdn
ef38ad7753
feat(web-shell): add read-only Excel artifact previews (#13664)
* feat(web-shell): add read-only Excel artifact previews

* fix(vscode): refresh notices for Excel preview dependencies

* fix(web-shell): bound Excel merge loading and preserve preview content

* fix(web-shell): bound Excel preview work and preserve recovery

* fix(web-shell): harden Excel preview values and recovery

* fix(web-shell): include bundled ExcelJS dependency licenses

* fix(web-shell): guard column expansion and complete ExcelJS notices

* fix(web-shell): complete Excel color fallback and review guards

* test(web-shell): follow artifact filename labels in Excel previews

---------

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-10-13 02:43:10 +00:00
qqqys
6775f24927
feat(extensions): add test failure report explorer (#13854)
* feat(extensions): add test failure report explorer

* fix(extensions): bound report snapshots before serialization

* fix(extensions): bound test report validation

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
2026-10-11 12:58:55 +00:00
Bob.qwencode
726e8d77f7
chore(release): v0.25.1 (#13904)
* chore(release): v0.25.1

* docs(changelog): sync for v0.25.1

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-11 12:00:47 +00:00
Shaojin Wen
2a69a2ff22
fix(managed-agent): commit oversized message records as inline chunks (#13375)
* fix(managed-agent): commit oversized message records as inline chunks

A Hosted answer past the 64 KiB inline resource limit streamed in full
through message.delta events and then failed its turn when the final
message.committed record exceeded the limit with OSS disabled. Split an
oversized record body into ordered managed-message-part resources behind
a managed-message-chunks manifest — the shape Hook message snapshots
already use — and reassemble on every read path (projection hot and cold
reads, the hosted SSE feed, workspace recovery). Each part stays under
the per-resource limit and the transaction stays within the server
budget, so a long answer completes instead of failing after publishing.

Fixes #13326

* fix(managed-agent): preserve long answers through recovery

Refs #13326. Builds on #13375 with the 64 KiB inline boundary, sequential chunk reads, and full-stack integrity regressions.

* test(managed-agent): allow streamed message settlement to finish

* fix(managed-agent): address long-answer PR review feedback

Strengthen recovery, streaming and transaction-budget regressions, wire the long-output E2E into CI, and clarify the supported persistence scope.

Refs #13326, PR #13375

* test: address long-answer persistence review feedback

* codex: address PR review feedback (#13375)

---------

Co-authored-by: jinye <djy1989418@126.com>
2026-10-07 19:40:46 +00:00
Shaojin Wen
4970bfa172
feat(managed-agent): adopt the next Hosted Harness generation (G3) (#13174)
* feat(managed-agent): adopt the next Hosted Harness generation (G3)

A Hosted Session no longer dies with the Harness process generation
that first served it (#12952 G3, Steps 1+2). On a generation signal
the control plane renegotiates once through the connector instead of
failing every bound Session until its own restart; pending Turns
re-attach through the takeover load as their retries come due.
Takeover refusals split into a typed terminal decline (deterministic
in the journal) and the existing retriable code; the takeover load is
idempotent against a lost reply and gets its own timeout; negotiation
requires the journal-contract feature token so a rolled-back Harness
is refused once; a marked-but-never-admitted Turn withdraws its
submission mark, backstopped by journal commandId idempotency.

The failover runner gains a Harness-only restart arm on all three
scenarios (harness_boot_id moves with no Java restart) and a
SIGSTOP/SIGCONT frozen former-owner fencing arm, wired into
hosted-harness-mysql together with the previously local-only basic
mode. Design docs included in English and Chinese.

* fix(managed-agent): tolerate the bootstrap 404 of a restarting Hosted Harness

The serve delegating app answers every routed path with a bare
pre-contract 404 while its runtime is still starting. The Harness-restart
arms raced that window: a prompt submitted milliseconds after the new
process bound the port could meet the bare 404, which the client read as
a generation contract violation and ended the Turn as
hosted_harness_protocol_error without any retry. A missing boot header on
404 now maps to a transient transport error so the coordinator retries
through the window, and the failover runner waits on deep health
(?deep=1) before driving turns into a restarted Harness. Terminal turn
failures now also log their underlying exception.

* fix(managed-agent): freeze only the Harness writer in the former-owner fencing arm

SIGSTOPping both original owners left the original Spring's workspace
binding unreclaimable: the reclaim proves death from /proc liveness, and
a stopped JVM still reads as alive there, so the replacement's reconcile
timed out (runtime_broker_reconcile_timeout) and the Turn never
completed. The arm now stops only the original Harness (reclaiming its
workspace binding requires the Spring to die anyway — the continuation
mode's crashProcess), completes the Turn with a full replacement, then
wakes the frozen writer and asserts the journal, transcript, and
session binding are untouched. Design D7 updated in both languages with
the discovered boundary: resource-level takeover of a surviving owner is
structurally out of reach today, which is exactly what this issue's
sequential-generation scope excludes.

* ci: retrigger sdk-java workflow after event delivery stall

* fix(managed-agent): pin the fencing proof on writer identity, not head revision

The first merge-base run of the freeze arm failed its own gate: after
waking the frozen Harness the journal head moved (rev 30 -> 45) while
the writer generation, boot binding, transcript, and terminal count all
held. The moved revisions were the replacement's own legal churn, not
old-writer mutation, so the head-equality assertion conflated the two.
The exit-check claim is identity: no transaction out of the old writer
generation after it wakes. Count that straight off
qwen_managed_session_journal_tx (which carries writer_generation), keep
the binding/transcript/terminal assertions, and align design D7 in both
languages. The error string now also reports the old-writer tx count.

* fix(managed-agent): qualify the journal tx schema in the freeze-arm gate

The identity-based old-writer count queried qwen_managed_session_journal_tx
unqualified; the runner's MySQL connection has no default schema, so the
arm died with ERROR 1046 instead of evaluating the fence. Qualify it as
qwen_managed_agent.qwen_managed_session_journal_tx like every other query
in this runner. No behavior change to the assertions themselves.

* fix(managed-agent): harden takeover correctness from the R1 review

- settle turn_settled checkpoints through the load route's own tail
  instead of a terminal decline, and keep transient store verdicts
  (missing_state/missing_checkpoint) retriable on both authorization
  reads
- exempt only takeover-shaped 409s from the pre-admission retry budget;
  every other failure kind meets it
- scope the 120s load-timeout to recovery loads so plain attaches under
  the CHM bin locks stay on 30s
- adoptGeneration: move map churn out of the connector monitor
  (lock-order inversion), evict only stale-boot entries on equal-boot
  mismatches
- answer a duplicate submit of an already-settled prompt with a 202
  replay anchored at its input.accepted sequence
- withdrawSubmissionAttempted: guard on the same status window as its
  peer CAS methods; complete capability mismatch terminally in both
  remaining coordinator layers
- runner: freeze-arm pre-freeze liveness assertion, /health-proven
  wake non-vacuity, teardown SIGCONT, exact-string dispatch-generation
  bounds
- CI: job ceiling 95 with corrected ceiling math; pin all seven
  failover arms in hosted-process-ci.test.js
- sync both design-doc languages with the shipped behavior

* fix(managed-agent): close the R2 review round on the replaceable-host-harness slice

- withdraw + cancel interference (R2-1, Critical): a CANCELLING Turn
  whose never-admitted mark was withdrawn is fast-cancelled instead of
  being re-dispatched into a second live execution at the adopted
  generation
- the generation-mismatch catch escapes the pre-admission budget only
  on a bound Session, like every sibling arm (R2-3)
- bound the remote-supplied decline reason to the error_message column
  so the terminal write itself never throws (R2-5)
- the last fail(...) in the catch ladder is failTerminally with the
  exception logged, like its siblings (R2-6); the withdrawal itself is
  logged too (R2-7)
- freeze arm: the replacement Spring inherits the original port so the
  woken writer meets a live fencing control plane, not a dead socket
  (R2-2); the wake block asserts the head actually advanced past the
  frozen owner's writer generation, drops the wrong heartbeat claim
  (R1-8 follow-up), separates the two disk-audit meanings, and asserts
  the retained Harness home (R2-8)
- the hosted /capabilities feature list derives from the registry
  instead of a hand-maintained literal (R2-9)
- docs: D7 scoped to the journal half of the Q2 exit check, the binding
  half and the pre-contract-404 classification recorded in Boundaries
  (R2-4, R2-10)

* docs(sdk-java): correct the load-timeout coverage comment in the client test

* fix(managed-agent): reference the frozen Harness through the children registry in teardown

The R1-4 teardown SIGCONT referenced the try-block-scoped `harness`
binding from the finally block, so every freeze-arm run died with
ReferenceError after all fencing assertions had already passed (CI run
37009843147). Reach the child through the children registry by its
start() name instead.

* docs(managed-agent): scope the adoption sentence to this slice's takeover reach

* fix(managed-agent): close the R3 review round at its five shared roots

- retract the turn_settled settle tail to a typed decline: the tail
  answered 200 with no recovery watermark (Java then failed the bind,
  orphaned a completed journal turn), fabricated state:'completed' on
  phase alone, ran mutating work under a passive load, and retained no
  snapshot for the lost-reply retry. D10 already owns
  rebind-and-keep-reading as the Step 3 row (R1-25-re, R3-2 .. R3-5)
- the load route's restore guard applies the same durable-vs-transient
  verdict read, because the bundle collapses both into one blocked bit
  and the durable decline was unreachable from production (R1-26-re)
- the pre-admission exemption keys on lease-shaped 409 codes only:
  configuration-shaped 409s (e.g. hosted_tool_profile_conflict) meet
  the budget, and the same 409 exempts nothing on an unbound Session
  (R1-1-re, R1-30-re, R3-13)
- equal-boot adoption keeps pendingRecovery: the markers were minted by
  the same live client (R1-29-re)
- lifecycle capability mismatch: revert completing-as-unconfirmed —
  that skipped the drain, flipped the session, and recorded a clean row
  with no FAILED vocabulary to tell it apart; the reason-loud retry is
  the honest end of the line until an operator realigns versions, and
  the README sentence says so. Action-response outbox keeps its honest
  FAILED-with-code terminal (R1-45-re, R3-5, R3-15)
- test hardening: concurrency close-once now surfaces worker assertion
  failures via Future.get and pins that no real client build happened
  (R3-12); loadTimeout-vs-requestTimeout discriminated at the call site
  (R3-9); single-walk acceptedInputSequence (R3-18)
- runner: 5-column execution queries pin runtime_session_id unchanged
  under --harness-only (R3-14); frozen-arm teardown looks the child up
  by a shared label constant and fails the run on a drift (R3-17)
- CI: ceiling 105 covers the nine summed step ceilings (92) plus the
  uncapped setup steps (R3-6); the seven-arm pin also asserts each
  script's flags and the install step's existence (R3-16)
- docs: both languages re-synced (D5 retraction, D9a code allow-list,
  D7 teardown, D8 runtime identity, honest validation digests, the four
  missing changes-table rows, README retry clause)

* fix(managed-agent): replay takeover snapshots only to a same-shape request

A passive load only asks broker.status(), so its snapshot keeps
phase='await_runtime'; replaying it to a drive load handed the
coordinator a checkpoint that isContinuationReady() can never accept,
terminally failing an otherwise continuable Turn. Record the request
shape with the snapshot and replay only on a mode match; the clear
conditions stay promptId-only.

Also make the adoption race test deterministic: a package-private
createClient() seam lets the test inject the replacement instead of a
real /capabilities call (proven flaky on CI), worker assertion failures
now surface through Future.get, and the pendingRecovery fixture seeds
its marker through the recovery path.

* fix(managed-agent): close the self-audit round on the replaceable-harness slice

Three rounds of undirected plus adversarial audit over the whole PR diff
(31 files), with three parallel deep-dive passes over the Java, TypeScript
and runner/docs surfaces. Every finding was re-verified against the tree
before acting; five reported defects were rejected on evidence.

Correctness:
- the load route read the blocked-restore verdict AFTER managed.close();
  sealing the journal makes every later store read fail as "writer is not
  active", which the authority erases into missing_state, so the durable
  branch this PR added could never fire and a corrupt checkpoint stayed in
  the retry-forever class. Read the verdict first, and pin the order with
  a witness that goes red when the read moves back (the local authority's
  seal does not disable reads, so only the ordering assertion catches it).
- drop hosted_session_already_attached from the lease-bounded 409 set: the
  daemon removes an attachment only on an explicit detach or delete and this
  control plane never detaches, so a Spring restart against a surviving
  Harness retried a permanent refusal forever instead of ending the Turn.
- adoptGeneration evicts by boot identity in both branches instead of
  clearing, so a marker minted by a concurrent rebuild can no longer be
  erased between two adjacent clears (which left a live attachment with a
  null recovery report), and markers follow their attachment.
- the post-withdrawal fast-cancel reads the row again: a cancel landing
  after the claim is invisible in the claimed snapshot, and acting on it
  submitted a live execution only to cancel it below.
- a durable verdict re-read after settling now declines as
  checkpoint_blocked, matching the identical pre-settle classification.
- an unrecognized blocked reason throws instead of declining, so a reason
  core adds later cannot silently become a terminal Turn failure.
- the operator-settable load-timeout is validated at connector
  construction; a bad value previously threw on every client() call and
  surfaced as an endless transient retry naming only the exception class.

Tests: already_attached and lost-withdrawal-CAS witnesses; the fast-cancel
witness now pins the re-read (cancel after claim); the concurrent-adoption
test surfaces worker assertions through Future.get and injects the
replacement through a package-private createClient() seam instead of
standing up a live /capabilities call; the CI pin gains each arm's mode
flag and a ceiling-sum invariant.

Docs (both languages): D1 now describes the boot-identity eviction and
lists what this slice really added to HostedHarnessClient; D3 no longer
claims the bind-refusal path stops being reached (it is D4's trigger); D6
records the same-shape replay clause; D7's fence rationale no longer blames
heartbeats (lease renewal touches writer_lease_until only); D9a names the
two exempt codes and why already_attached is not one; the validation digest
states which claims are pinned and which are not; the CI arithmetic reads
12 + 8x10 = 92; the stale workflow nit is dropped.

* chore(ide): regenerate NOTICES for the merged dependency set (email channel)

* fix(ide): replace the workspace-polluted NOTICES with the pristine regeneration

The file committed in f7a960c559 was generated under a polluted module
layout left by an aborted install: its resolution hoisted
is-fullwidth-code-point@5.1.0 and produced 663 total dependencies, so
CI's own regeneration (pristine: 662) mismatched and the up-to-date gate
failed. Replace it with the pristine worktree's output, which the gate's
own regeneration produces to a no-diff state.

* fix(managed-agent): close the R4 review finds on the takeover and refusal paths

- classify missing_checkpoint and a bare missing_state as durable verdicts
  so a permanently unrecoverable session declines instead of being retried
  past the lease window; only a missing_state carrying the erased store
  error's message stays retriable
- map a fenced predecessor's managed_session_writer_conflict to the coded
  retriable 409 in the load route so the wait provably ends with the lease,
  and allowlist that code in LEASE_BOUNDED_409_CODES (dropping the inert
  hosted_prompt_recovery_required, which can only arrive post-mark where
  the exemption gate is already bypassed); null-guard the membership test
  against codeless error bodies
- replay a takeover snapshot only to a request whose parsed store identity
  matches the attached session's, never to any takeover-shaped caller
- fetch the harness client AFTER resolving the attachment at the six entry
  points, so an adoption completed during a create/load round trip cannot
  strand a call on the closed client
- pin the reply watermark of the journal-replayed prompt admission, the
  construction-time loadTimeout guard, the marker-eviction half of
  adoptGeneration, and the withdrawal CAS ordering with witnesses, and
  widen the load-timeout test's durations so the capabilities negotiation
  keeps headroom on contended lanes
- narrow the README's typed-decline sentence to the surviving row and
  generation binding, and mirror the exempt-code list in both design docs

* fix(managed-agent): answer the round-5 waits without terminalizing them

- a takeover now splits three ways: drivable answers recovered, a
  deterministically unrecoverable drive keeps its typed decline, and
  anything whose wait belongs to someone else (a requested approval, or
  the whole non-drivable space on a cancellation-only load) answers
  inapplicable so the route falls through to the plain attach — a
  user-cancelled Turn cancels instead of failing as
  managed_runtime_recovery_blocked, and an approval wait survives its
  generation rather than dying while the answer is still deliverable;
  the coordinator keeps the await_action guard as a retriable path even
  if a Harness still declines it
- an unconfirmed cancellation re-drives with its snapshot restored: the
  cancel admission's rollback is now symmetric, so the retrying takeover
  load replays instead of being refused already-attached
- an unrecognised blocked reason at the final re-read stays retriable on
  the drive side like the pre-settle read, instead of terminalizing on
  unresolved_after_settle; only durable verdicts may end a Turn
- the hosted persona's curated tag set is declared once in the registry
  (hostedPersona descriptor flag) instead of being filtered by a third
  hand-maintained list at the route, pinned by the docs-contract test
- the harness-only arms pin a surviving worker through the runtime
  session row's own liveness metric, not the insert-frozen id column
- witnesses: declined reason on the wire, cancel-report replay after a
  lost answer, inapplicable shapes at the recovery kernel (both loads),
  the drive disjunct of the load-timeout predicate, the one rebuild per
  adoption race, and coordinator corners for cancelling/await_action

* fix(ci): qualify the runtime-session heartbeat query with its schema

The harness-only arms' liveness beat read qwen_runtime_session bare;
mysql runs without a default database, so the Harness-restart in-flight
arm died on ERROR 1046 with 'No database selected'. Every other probe in
the runner already addresses qwen_managed_agent.<table> — this one now
does too.

* fix(managed-agent): close the round-6 shape and wedge findings

- the pre-kernel no-tool refusal and the restore guard now honour the
  same shape rule the kernel does: a cancellation-only load answers
  inapplicable and attaches plain (or the baseline retriable refusal
  where no attach is possible) instead of terminalizing a Turn the user
  asked to cancel; a takeover that answers inapplicable no longer trips
  the bare-load refusal on its empty recovery
- the lease-bounded exemption is keyed on the writer lease's own wire
  code only: hosted_turn_recovery_required covers arbitrary takeover
  failures, and exempting it wedged durable refusals in an unbounded
  pre-admission retry; a refusal carrying that code now meets the budget
  like every other body shape while managed_session_writer_conflict
  keeps its D9a window
- witnesses: the no-tool plain-attach coincidence for cancel vs the
  typed model_start decline for drive, the durable restore verdict on
  the cancellation arm, and the budget-hit for a durable 409 on the
  bound path (the old takeover-409 premise rewritten, its writer-
  conflict twin already covered)

* fix(managed-agent): settle a cancel first on a plain-attached replacement

A real-stack round-4 wedge: cancelling a Turn whose Harness died mid
model wedge (Work realized after the merge) failed as
hosted_harness_generation_mismatch — the plain attach leaves the bind
CAS unwinnable (boot-old != boot-new), the cancel was never issued, and
everything queued behind the Turn then retry-forever: later prompts
409, a close 409, no public-API escape.

runClaimed's recurrence without recovery now cancels before it binds:
a CANCELLING Turn does not submit, and a bind exists for submissions —
the next Turn's takeover adopt moves the generation when something
actually continues. cancelledOnAttach suppresses the tail's later
re-cancel. The R5 witness now also pins: bindHarness is never asked,
withdrawSubmissionAttempted is never touched, and the Turn is neither
recovery-blocked nor generation-mismatched. Design docs D3/D4 gain the
bypass rule in both languages.

* fix(managed-agent): close the inapplicable aftermath the round-8 review found

- an inapplicable takeover must not latch session.blocked — a 200 answer
  that refuses every settlement route is a lie (R5-2's latch regression:
  nothing outside settleCancelledHookTurn clears it for a non-hooks
  session). The blocked latch now splits the same shape rule: only a
  genuinely unsettled takeover parks there, not an inapplicable answer.
- a plain attach of a bound Session after adoption rebinds the Session
  row with its own CAS (owner + live lease + expected = the row's boot),
  since bindHarness provably refuses a turn that already posted its epoch
  — the Turn is neither generation-mismatched nor stranded (R8-1).
- a Turn written turn_settled completes, so it is never stamped as
  managed_runtime_recovery_blocked: the kernel answers inapplicable on
  BOTH load shapes (the plain attach lets the daemon's own projection,
  not a declined verdict, write the terminal record), retaining the
  rebind-and-keep-reading rationale for Step 3 (R8-2).
- the #13388 pinning test's mock endpoint now advertises
  managed_session_journal_delta_v1 like every other Harness fixture of
  this branch — a merger's follow-up angry about the gate (D9c), not an
  upstream flake.
- witnesses: turn_settled pinned as inapplicable on both shapes at the
  kernel; plainAttachAfterAdoptionRebindsBeforeItSubmits asserting the
  store CAS adoption; R5-2's corner witnessing 200 without
  recoveryRequired; coordinator 46/46, hosted client + pinning tests,
  TS session+recovery fully green.

* fix(managed-agent): close the inapplicable aftermath the round-9 review found

- the no-tool arm now refuses with the baseline retriable one again
  instead of minting a plain attach whose parked Turn nothing may
  resolve (R5-2'), and a takeover load without broker options shares
  that refusal;
- the journal prompt replay answers 202 only when the request body's
  digest proves identity against the admission its input.accepted
  holds; carrying the same Id with different bytes is a real
  prompt_conflict, never a silent swap of watermark (R9-1);
- a plain attach after adoption moves the Turn's stream epoch through
  recordRecoveryAdmission to the attach's epoch, on both the binding
  ladder and the cancel-on-attach arm — a stream opened with the
  claimed epoch would be rejected on the new generation as epoch
  mismatch (R8-1').
- witnesses: the no-tool cancel arm refusing retriably while drive
  declines with its typed reason, the digest mismatched replay timing
  out as hosted_prompt_conflict, and R8-1 streaming the adopted
  generation's epoch on both arms (plainAttachMoves* and
  cancelOnAttachStreams*) with recordRecoveryAdmission pinned.

* deflake: carry #13403's pinning fixture under the journal-delta gate + harden the 13328 mount-holder waitFor

The merge of #13403 turned the Java CI lanes red with
DaemonProtocolException: Endpoint does not advertise
managed_session_journal_delta_v1 on every burst caller: this branch's
D9c capability gate (from #13388's pinning work) rejects endpoints
without that token, and #13403's new /capabilities fixture did not
advertise it. Same fixture-parity fix as #13388's — the token is a
fixture property, not production behavior; the deliberate negative
stub (capabilitiesJsonWithoutJournalToken) stays as the gate's
witness.

The ubuntu Test lane red on the same head was the issue-13328
mount-holder vi.waitFor defaulting to its 1 s window against the
admission pipeline on the loaded runner ("expected undefined to be
defined", repeated across both retries). Give it an explicit 10 s
timeout, assertions unchanged — the same deflake pattern as
#13411/#13323/#13430.

* fix(managed-agent): close the R11 review round (inapplicable pays only what it can)

R11 (qwen-code-ci-bot review, 5 threads, 3 new + 2 re-checks) landed on
the shared consequence of R10's fixes: an inapplicable/plain-attach answer
stays honest only where a settlement route can pay.

- R11-1: the prompt route's session-scope guard now answers the
  SESSION-level wedge code (hosted_turn_recovery_required); the
  prompt-scoped code names only the requested prompt's own unsettled
  duplicate, so the duplicate-admission adopt branch can no longer be
  forged by a refusal about someone else's parked Turn — and the budget
  treats the session-level code as pre-admission whenever the Turn
  provably recorded no epoch, recording the wedge's own code on
  exhaustion (provenance-gated on an existing submission mark).
- R11-2 / R10-1 re-check: both takeover arms compute the bare branch's
  settle projection inline (`settleProjectablePromptId`): approval waits
  keep the plain attach (resolve pays), turn_settled is settled by the
  load itself via the extracted `runSettleProjection`, and an unpayable
  park keeps the baseline retriable 409 instead of a healthy-looking 200.
- R10-2 re-check: the kernel's passive inapplicable is narrowed to those
  two payable states; initial / durably blocked / model-start / unknown
  phase / wrong-checkpoint throw into the retriable refusal.
- R11-3: createOrLoad's final attachment put restores the pendingRecovery
  marker for snapshot-carrying passive re-attaches, or the cached branch
  of a later recoverManagedRuntime answers "nothing parked" over a
  snapshot it holds.

Witnesses: 245/245 CLI across the three hosted suites (2 new route
witnesses + updated session-code assertion + 2 recovery-suite flips),
QwenHostedHarnessConnectorTest 34/34 (marker witness),
HarnessCoordinatorTest 52/52 (session-busy witness; R6 budget witnesses
pin the load side as recorded). Docs updated in both languages.

* fix(managed-agent): settle the no-tool cancellation takeover instead of refusing forever (Arm B)

The round-6 real-stack wedge: a Turn with no Runtime work parked mid
model round, its Harness generation dead, cancelled — the no-tool arm of
the takeover branch answered the baseline retriable 409 on every
cancellation-side redispatch, and with the Turn still carrying its
submission mark, the retry budget never fired. Turn 2 stayed CANCELLING
forever and every later Turn met the same wall.

The fix keeps the wire honest and the settlement explicit:

- LoadHarnessSession carries `cancellationTakeover`, set ONLY by
  recoverManagedRuntime's cancellation arm. A plain passive re-attach
  shares the passive wire shape and deliberately never sets it — nothing
  may mint a canned CANCELLED record for a wait whose owner could still
  exist.
- With the signal present, the no-tool arm of the takeover branch
  settles the ownerless park into the daemon's journal itself
  (turn_result/cancelled, landing turn.settled); the writer fence on the
  very load is the safety boundary (it proves the producing generation
  can never write again). On write failure the arm keeps the baseline
  retriable 409. Without the signal the arm keeps its merge-baseline
  answers — retriable 409 on cancel, typed model_start decline on
  drive — so the collision guard holds by construction.

Witnesses: `settles the cancellation of an ownerless parked no-tool Turn
on the takeover load` (200 plain attach + durable settle tag + cancel
reads settled-at-tail as 204 + next prompt admits; the revert flips the
verdicts red) with the collision-guard passive-only rerun still at 409,
plus wire-side `carriesTheCancellationTakeoverFlagOnlyOnTheCancellationLoad`
asserting the flag travels only with the signal.

Verification: TS hosted suites 259/259, qwencode 32/32,
QwenHostedHarnessConnectorTest 34/34, HarnessCoordinatorTest 52/52,
typecheck, eslint, prettier. Design doc updated in both languages.

* fix(managed-agent): converge P1-1 (tool-config vs unpaid Runtime work) and P1-2 (stale approval copies)

Round-7 re-review of 275f934824 found two remaining wedge families
inside the cancellation settlements this PR introduced. Both converge on
durable proof, never on the stale checkpoint copy.

P1-1: a takeover cancellation must separate "the Session is configured
for tools" from "the Turn owes unpaid Runtime work". A files-profile
session whose owner died BEFORE its first tool call carried no unsettled
Runtime work (checkpointless or a bootstrap checkpoint naming no Turn) —
and the kernel's different-Turn check threw on the bootstrap's null
turnId, so the load answered the retriable 409 forever. The load route
now hoists that split ahead of the kernel: with the explicit
cancellationTakeover signal (and only with it — the collision guard
stays), any ownerless park without unpaid Runtime work settles into the
journal itself; work in flight follows the recovery-cancel report; the
no-signal shape keeps its baseline refusal unchanged.

P1-2: the durable action record — not the checkpoint's stale copy of the
approval — decides an owner-died approval. When the record says the wait
ended, the recovery kernel no longer maps it to the inapplicable plain
attach: expired/cancelled is transient (route refusal), decided is
advanced through its own durable gate ONLY on a drive load (a
cancellation never crosses it), and every other shape (still requested,
or undefined) keeps the plain attach a resolve route can pay. The
cancel/expired arrive by the same separation (settle with the signal;
decided is settleable with the signal too — a CANCELLING takeover does
not resume approved work no matter which side the user took).

Witnesses: `settles the cancellation of an ownerless parked Turn on the
takeover load (profile=null|FILE_PROFILE)` parameterized exactly as the
re-review demanded; three recovery-kernel witnesses for the cross-read
(resolves-inapplicable only while requested; throws for ended-without-a-
decision; drive declines checkpoint_blocked on an undrivable decided
wait); route-level `settles a cancellation takeover whose durable
approval already ended (state=cancelled|decided)`. Design doc updated in
both languages. Suites: hosted-harness-session 222→(+10 new),
hosted-runtime-recovery 36→(+3 new), issue-13328 33→, all green;
typecheck and eslint clean.

* chore(vscode-ide-companion): regenerate NOTICES for the merged dependency layout

* fix(managed-agent): read owed Runtime work off the work itself and pay coded plain-cancel refusals with the cancellation takeover (R9)

Round-9 real-stack measurement found the cancellation separation still
wedging on two families: every cancelled Turn after the first, on both
profiles, and every cancel row of P1-2 — where no load ever left the
coordinator, because a plain-cancel retry can never settle a parked
Turn and no redispatch was forthcoming.

- The park gate now reads owed work off the work itself: a checkpoint
  naming an earlier Turn whose tool items all settled and were consumed
  owes nothing to a cancelled Turn that never reached a tool call; and
  a no-tool Session cannot owe Runtime work by definition, so its
  cancellation arm settles unconditionally again (the round-8 behavior
  the gate had displaced onto a blocked restore basis).
- The admitted-cancel path turns a plain cancel's coded 409
  hosted_turn_recovery_required into an immediate cancellation
  takeover load over the same attachment (recoverManagedCancellation),
  and the connector sends it even with a healthy cached attachment —
  the shortcut would swallow it and the load would never leave the
  process. A recovered Runtime park it reports is cancelled through
  its checkpoint admission; the already-running stream lands the
  settle either way.

Witnesses: hosted-harness-session settles the second-Turn cancellation
park on both profiles (the round-9 rig witness adopted); the
coordinator drives the takeover load exactly on the coded refusal
(plain park and recovered park) and on no other answer; the connector
always sends the cancellation load over a cached attachment. Suites:
hosted 229, HarnessCoordinatorTest 55, QwenHostedHarnessConnectorTest
35, ManagedAgentServerIntegrationTest 29.

The decided-approval drive arm of P1-2 (rows where the user answers
allow or the approval expires unanswered) remains open: it needs the
plain-attached replacement to drive the parked Turn itself once the
durable wait ends, and lands as its own slice.

* fix(managed-agent): pay the cancellation signal identically on an attached re-answer (R9-2)

A cancellation takeover load the coordinator forces onto a Session
ALREADY attached in this daemon used to skip the entire settle
separation: the attached branch called the passive kernel directly,
which never advances a durable wait passively — so a park whose
approval ended after the attach (the round-10 allow, then cancel
repro) was thrown back as an unknown phase and refused retry-invariant,
while the first-load branch settled the identical shape. Hoist the
separation above the attachment split: the attached branch now reads
the signal too and runs the same no-tool arm, owed-work gate and
durable cross-read, settling through settleCancelledHarnessTurn into
the journal the already-running stream lands.

Witnesses parameterize both ended states: attach plain while the
durable approval says requested, end the wait after the attach with
the stale checkpoint copy untouched, then redrive with the cancel
signal — 200 with the redriven-load settle log, and the plain cancel
route reads settled-at-tail. Suites: hosted session 231, recovery 44.

* fix(managed-agent): close the durable wait before the cancelled terminal lands (R9-3)

settleCancelledHarnessTurn only wrote turn_result/cancelled into the
journal, and the record sink's turn.settled only advances next-turn
checkpoints at a model-start phase — so a cancelled Turn parked at an
approval wait ended in the journal while its checkpoint stayed at
await_action, and the Session's next prompt met
`ManagedHarnessBlockedError: await_action is not a model-start phase`
(the round-11 repro: old Turn completed cleanly, new Turn failed as
hosted_turn_failed before the model was ever called, closing 1 P1).

The cancelled settle now closes the ended wait first through the
wait's own durable gate (resolveDurableWait → model_output_committed,
a model-start family phase) whenever — and exactly when — the durable
approval already ended, mirroring the cross-read the caller's gate
performed. The decision remains the USER's and nothing resumes: the
Turn dies immediately after. Runs of requested waits or of in-flight
Runtime work keep the kernel's existing paths.

Witness (both answers): requested approval attaches plain on the
passive takeover, the record ends after the attach with the stale
copy untouched, and the attached cancellation takeover settles —
200 with the redriven-load settle log, plain cancel reads 204, and
the settle provably commits the model_output_committed boundary
ahead of the terminal record. Suites: hosted session 231, recovery
44; typecheck, prettier, eslint clean.

The unit suite cannot stage "approval-first park + literal owner
death" today (DELETE refuses active Turns, and abort unwinds the
wait cleanly), so the end-to-end relief-Turn acceptance stays with
the real-stack probe; the annex-level witnesses cover the settlement
payment and the wait-close commit.

* chore(vscode-ide-companion): regenerate NOTICES from a clean-room install

The previously committed file was generated in the long-lived
worktree whose pnpm layout had drifted (is-fullwidth-code-point@5.1.0
+ get-east-asian-width@1.5.0 + fdir/picomatch slot differences) even
though pnpm-lock.yaml is byte-identical to main. Regenerated in a
fresh worktree with a frozen-lockfile clean install: 662 deps, and
the output matches the lint lane's renewed file byte for byte, which
the polluted layout could not reproduce.

* fix(managed-agent): pace forced takeover loads per Turn and make the cancelled settle idempotent (R9-4)

Two round-10 verdict items:

1. The coded-409 escalation (R9-P1-2) ran on every ~500ms lease-renewal
   tick, so a Turn cancelled while its wait still owns a live durable
   record paid ~4 requests/s of daemon work forever (21 forced loads and
   20 cancels in the verdict's captured window). cancelAdmittedTurn now
   paces the forced cancellation takeover load per Turn (5s minimum
   interval): the cheap plain-cancel retry keeps carrying the wait, and
   a wait that ends inside the interval still settles on the next paced
   attempt — far below the approval timeout's 45s budget.

2. A settle that already landed was not idempotent: the coordinator's
   next forced load inside the stream's landing window met
   `event id turn:<id> is already committed` (3/3). The cancelled settle
   now re-reads the journal's own turn.settled for this prompt before
   writing, so the redriven load is a re-answer, never a second
   turn_result racing the event-id CAS.

Witnesses: three paced coded-refusals (t0 / t+500ms / t+5s) yield three
plain cancels but two forced loads (HarnessCoordinatorTest 60); the
round-9-2 witness pair now races a second identical load inside the
landing window and asserts one cancelled record and no
takeover_unavailable line (hosted suites 275).

* test(managed-agent): commit authority-valid deltas in the restore byte-budget test

How: holdsRestorePagesInsideThePerPageByteBudget now builds each dense
transaction from 210 well-formed message.delta lines (text at the
reader's 4096-byte cap, ~1 MB per transaction) plus the commit marker,
through a shared TurnEventLines composer, and asserts that revisions
1..9 fit the 8 MiB page budget while a tenth does not. The reader replay
IT now also replays that delta shape. The class turns off MockMvc
print-on-failure.

Why: #13348 added the test with placeholder lines ({"subtype":"event_v1"}
plus a padded commit_v1 line), and #13355 (merged later, never run
against it) made the commit gate refuse any non-event line inside the
event range, so every commit after genesis answered 409 and main's SDK
Java lanes went red. The failure was masked as timeouts: Spring Boot
printed the failed test's ~1.3 MB request body as one log line, which
stalled the Actions log pipeline for ~45 min, so the Hosted Verify and
O4 steps blew their ceilings and the MariaDB lane hit its job timeout.

Test: ManagedSessionStoreIntegrationTest and ManagedExtensionRecordStoreTest
green; full managed-agent-server surefire (1077) and checkstyle green;
HostedCommittedEventLineReplayIT green through the real TS reader and
red when the delta text exceeds 4096 bytes; dropping the page byte
budget in ManagedSessionStore fails the test (10 rows instead of 9)
with no log line over 400 chars.

* fix(managed-agent): propagate the settle's own wait-check fault instead of degrading it to a successful cancel (R9-5)

The cancellation settle's authorization re-read was wrapped in
`.catch(() => undefined)`, so a transient store fault surfacing
between the caller's gate and the settle's own wait-check degraded
into 'nothing to close' — the cancelled terminal landed over a wait
the fault hid, and the Session's next prompt then found the same
obsolete checkpoint that family wedges on (the round-11 fault
injection at this exact boundary: authorization flips
runnable → blocked/missing_state → runnable once, the settle still
mints turn_result, and the run dies on `await_action is not a
model-start phase`).

A read failure is a retriable store fault, not 'nothing to close': it
now propagates into the caller's try-catch, which keeps the baseline
retriable refusal — the fault's own retry ladder owns the retry, and
the replayed shape then settles identically.

Witness mirrors the probe: the wait-gate reads fine, the settle's own
check faults once — load refuses hosted_turn_recovery_required with no
Cancelled record and an honest 409 on the plain cancel, then the same
shape settles after the fault clears (settle log, the
model_output_committed boundary, plain cancel 204). Suites 276/276
(hosted session 232, recovery 44), typecheck/prettier/eslint clean.

* fix(managed-agent): refuse the cancelled settle on fault-shaped authorization verdicts too (R9-5')

R9-5 propagated THROWN authorization faults into the caller's retriable
refusal, but the authority never throws the same class of failure: the
real path takes a retry-exhausted ManagedSessionStoreTransportError and
converts it in place into `{status: 'blocked', reason: 'missing_state'}`.
From there the cancelled settle still marched on — it skipped the wait
advance and wrote the cancelled terminal anyway, spending the Session's
next prompt on the obsolete `await_action` checkpoint the verdict hid
(the round-11 verdict at `964b87a50a`, reproduced with the production
error type: 200 on the takeover, the terminal lands, and the next turn
dies `await_action is not a model-start phase`).

Fault-shaped authorizations are never "nothing to close":
`missing_state` (the TransportError conversion), `opaque_state` and
`invalid_state` now all answer the same retriable refusal — the settle
refuses rather than minting what it cannot prove. `missing_checkpoint`
stays payable on purpose: it is the Arm B park's honest form (a no-tool
Session with history provably has no checkpoint) and its settle is the
already-verified behaviour.

The witness covers both faces of the store fault
(`it.each(['exception', 'missing_state'])`): same park, same stage,
either a thrown fault or a folded `blocked/missing_state` — the load
refuses with zero settle and an honest plain cancel 409, and the replay
settles the moment the fault clears. Suites 277/277 (hosted session
233, recovery 44), typecheck/prettier/eslint clean; design docs note
the boundary in both languages.

* docs(design): split the R9-5' verdict paragraph into its own bullet

The R9-5' text rode mid-bullet after R9-5's run-on, which Prettier
then flattened to column zero — breaking the list structure the lint
lane rejects outright. Both languages carry the split now, and the
lint-facing check passes again.
2026-10-07 03:38:25 +00:00
Bob.qwencode
8d864a9f21
chore(release): v0.25.0 (#13448)
* chore(release): v0.25.0

* docs(changelog): sync for v0.25.0

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-05 10:07:16 +00:00
Shaojin Wen
aca4d03c7e
feat(managed-agent): Add dev:managed-agent one-shot dev launcher (#13218)
* feat(managed-agent): Add dev:managed-agent one-shot dev launcher

Developing against the Managed dual-path WebShell previously meant
starting an ordinary daemon, a private Hosted Harness, and the Spring
control plane by hand, keeping their ports, tokens, and capability
digest consistent by copy-paste.

The new launcher mirrors daemon-dev: it serves the daemon and the
Hosted Harness from TypeScript source with rotating tokens and digest,
writes the Spring wiring to the git-ignored (0600)
.qwen/managed-agent-dev/spring.env, waits for /actuator/health on the
Java side (--skip-java-wait bypasses), then opens the WebShell Vite dev
server with the Managed panel selected. Ports auto-increment when busy,
and any child exiting aborts the group so the stack cannot half-start.

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

* fix(managed-agent): Harden dev:managed-agent against review findings

Round-1 review found 4 Critical and 14 Suggestion-level issues in the
launcher; this commit addresses all 18:

- Windows teardown now uses taskkill /T so the dev.js grandchild that
  owns the sockets cannot be orphaned (R1-1).
- Equal --daemon-port/--harness-port is rejected, the daemon probe
  excludes an explicit harness port, and both health gates assert
  responder identity through /capabilities (the harness must present
  this run's capability digest), so a busy-port bump can no longer be
  reported as a healthy mis-wired stack (R1-2).
- The two serve children boot in serialized stages: dev.js destructively
  re-stages the shared browser-use runtime on every boot, and the
  parallel spawns raced on it (R1-3).
- The token-bearing open path travels via QWEN_WEB_SHELL_OPEN_PATH,
  consumed as server.open in the web-shell Vite config, instead of a
  --open argv that npm echoed and cmd.exe would split on '&' (R1-4,
  R1-17).
- spring.env moved to the OS temp directory (mode 0600) so the harness
  bearer token no longer sits inside the served workspace; a PowerShell
  sibling is written for Windows, a warning fires when a previous run's
  file (and a possibly stale Spring) exists, and the Spring recipe now
  carries the datasource exports and one-time sibling installs in both
  README and the startup banner (R1-5, R1-6, R1-11, R1-12, R1-14).
- --tenant is validated against Spring's own pattern, waitForHttpOk
  carries the last HTTP status/error into its timeout, a Java timeout
  no longer tears down the healthy local stack, signal handlers live
  inside the entry guard, and the isMain guard is symlink-safe (R1-8,
  R1-9, R1-10, R1-16, R1-18).
- Exported builders (kill plan, harness wiring, serve stages, web
  launch, URL) with mutation-witness tests pin the contracts the first
  round showed unpinned (R1-7, R1-13, R1-15).

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

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

* fix(managed-agent): Probe the web port on Vite's bind family

Vite binds 'localhost', which resolves to ::1 only on IPv6-first hosts,
but the web-port probe listened on 127.0.0.1 and called a held port
free — observed in a two-launcher dry run where the second launcher's
probe passed and Vite silently bumped past the probed value (the
printed URL then names the wrong port; the TTY URL also used the IPv4
spelling that cannot connect to an ::1-only bind). The probe now takes
a host parameter and the web probe uses 'localhost', matching Vite's
own bind attempt by construction; the printed URL uses the localhost
spelling. A unit witness binds a server on 'localhost' and requires
the probe to walk past it.

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

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

* fix(managed-agent): Close round-2 review findings on dev:managed-agent

The second review round (2 Critical, 8 Suggestions) found the round-1
remedies themselves creating new edges; this commit addresses all ten:

- The env-file write is now fail-closed: the directory is keyed by uid
  and a hash of the checkout root (multi-user and multi-worktree runs no
  longer collide or cross-truncate), created 0700, verified owned, each
  target lstat-checked and refused when it is not our regular file (a
  planted symlink is bounced, its victim untouched), and 0600 is applied
  by chmod so it holds on the overwrite path too. POSIX-only
  enforcement: NTFS owns the boundary on Windows, where stat mode masks
  and chmod read-only would have broken the launcher outright (R1-11).
- Teardown now covers SIGHUP plus a synchronous exit hook, so a closed
  terminal tab or dropped SSH session no longer orphans the credentialed
  serves that detached:true would strand (R2-1); registrations stay
  inside the entry guard and are pinned via TEARDOWN_SIGNALS.
- Explicitly pinned ports are probed and rejected when busy, because
  qwen serve bumps on EADDRINUSE regardless, which previously meant a
  60-second misleading timeout (R2-2); Vite gets --strictPort and a
  pinned --host so it fails closed instead of drifting past the probed
  port (R2-3).
- waitForHttpOk captures err.cause.code (ECONNREFUSED now visible
  through undici's generic 'fetch failed') and clears stale accumulators
  so a fresher error supersedes an older response (R1-9 residual).
- The Java gate applies its siblings' identity logic through
  springHealthExpectBody instead of certifying any 2xx (R2-6).
- The PowerShell recipe line is uncommented and self-enabling
  (Set-ExecutionPolicy -Scope Process), emitted by an exported
  buildSpringRecipeLines that also carries the database/user creation
  step matching the reference e2e, mirrored into the README (R1-14,
  recipe gap).
- Effectful decisions moved into exported, injectable helpers
  (executeKillPlan, runServeStages, ensurePortFree) with interleaving
  and argv witnesses, and nine loose oracles tightened (rotation, ::1
  family fixture, daemon argv, launch env/argv, sharper retry message,
  the vite env-consumer case in web-shell, the Authorization header, the
  harness port in both base URLs, ambient-credential collision) (R2-4,
  R2-5).

Dry-run witnesses: staged healthy with digest pairing, recipe lines and
the env-file layout (0700 dir, 0600 file, no ps1 on POSIX) verified,
pinned-busy fails in milliseconds with exit 1, and a live SIGHUP drains
daemon, harness and Vite with no orphaned listeners.

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

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

* fix(managed-agent): Skip the web-shell spawn once teardown has begun

Self-audit after the round-2 fixes found two edges: a child dying
during the Java wait now lands in the non-fatal continue branch, which
would print the Managed URL and spawn Vite into a group teardown was
already reaping (the exit hook caught it, but the output misleads); and
killChild's re-entrancy comment overclaimed a no-op second pass — the
group/taskkill plans do not set child.killed, so a second pass is a
redundant signal to a dying tree, which is safe but not a no-op.

Guard the spawn on shuttingDown and correct the comment. No behavior
change on the happy paths; the script suite stays at 60/60.

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

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

* fix(managed-agent): Close round-3 review findings on dev:managed-agent

The third review round (4 Critical, 18 Suggestions), including three
defects the round-2 fixes introduced, now addressed together with the
review's batching recommendation:

- shutdown() counted only signal-dead children (the >>continue<<
  predicate read 'signalCode === null'), so Ctrl-C/SIGHUP launched
  process.exit immediately, orphaned every live server, and a sole
  signal-killed child exited 0. The probe now reads isLiveChild, which
  the suite pins in both directions (R2-1).
- The credential writer's checks resolved through symlinks: a planted
  link at the directory walked the lstat refusal, and a dangling link
  at a target skipped it entirely. The writer now lstats everything and
  refuses any symlink up front (R1-11), with planted-link victim-
  untouched cases.
- Teardown now hears SIGQUIT (Ctrl-\) and escalates to SIGKILL after
  the 5 s grace instead of just exiting (R2-1/R3-1); the exit-hook
  comment narrows to what an in-process hook can promise instead of
  claiming coverage for unhandled signals and hard aborts.
- Pinned-port checks moved from launcher start to each stage's own
  spawn step, so a port taken between the daemon's 60 s boot and the
  harness's spawn can no longer be silently bumped past (R2-2); the
  web port resolves at spawn time through an exported launch seam, so
  a squatter in the Java window cannot turn --strictPort into a full
  teardown (R2-3, R3-2).
- The Java gate's teardown path is silent: no 'stays up' continuation
  line while children are reaping (R3-4), extracted into a runJavaGate
  seam with an injected health wait so the path is test-reachable.
- healthFailureAction left only a literal-'continue' call site; the
  dead policy table, the unreachable abort branch and its describe are
  removed per the review's KISS option (R3-5).
- The MySQL recipe grants the 'qwen' user at both 'localhost' and
  '127.0.0.1' (official images enable skip-name-resolve) with a
  container-topology note, and a pairing test ties the granted
  hosts/password to the exported SPRING_DATASOURCE_* literals (R2-7,
  R3-10).
- Test-oracle batch (R3-3, R3-6..R3-9, R3-14, R3-15): default-runner
  witness for the kill plan, detached flag via an exported
  spawnDevProcess, first-run mkdir 0700 gate, stalled-responder
  rejection, both uid arms, stages[1].args pin, : prefix pins for
  the ps1 BASE_URL/digest lines, the printed ps1 path tied to the
  writer's filename, '503' replaced by the port-safe 'last response'
  suffix assertion with an in-range budget and a missing-finally close,
  the ephemeral-port-safe family fixture (property, not exact port),
  usable-on-Windows branch-shape cases decoupled from POSIX mode
  assertions, and parsed-actionable-line checks for the PowerShell
  recipe. ensurePortFree's unused probeHost parameter is gone (R3-13),
  and the env-transport comments now record the browser-hop argv the
  token still rides (R3-12).

78/78 in the script suite; live dry run healthy (dual-user recipe,
argv clean), and a real SIGQUIT drains daemon, harness and Vite with
no orphaned ports.

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

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

* fix(managed-agent): Wire the Session Store every Managed Turn needs

Real-stack verification on the PR thread found F1: since #12754 the
Hosted Harness answers 400 invalid_managed_session_store to any POST
/session without a managedSessionStore descriptor, and Spring only
sends one when its HTTP Session Store is enabled — so the launcher's
recipe brought up a perfectly healthy stack in which every Managed
Turn failed with hosted_harness_rejected. The stub-Spring dry-run and
env-name checks in earlier rounds never drove a real Turn through the
wiring. Adopts the reviewed candidate patch (wenshao assets-pr13218
@ ef748da, measured 3/3 Turns against a freshly created DB) and adds
the remaining notes:

- spring.env/ps1 now also carry QWEN_MANAGED_AGENT_SESSION_STORE_*
  (enabled, base URL = --java-url since the Harness calls Spring back,
  and a dev workspace id), so the recipe by itself produces a usable
  Session Store (F1).
- SERVER_PORT is emitted from --java-url for plain-http loopback URLs,
  so a non-default --java-url no longer leaves Spring on 8080 while
  the launcher waits elsewhere (N2).
- Thrown messages no longer carry the '[managed-agent-dev]' prefix that
  main()'s catch prepends once (N3).
- '--java-url' is trimmed before validation, so surrounding whitespace
  can't pass the checks and then fail every health attempt (N5).
- The README states the hosted_harness_rejected diagnostic mapping
  (Harness 400 = Session Store not loaded; 401 = stale Spring) (N1).
- The first-run 0700/0600 writer case is re-gated for POSIX semantics
  (R4-1); content/branch-shape cases keep running everywhere.

The supervisor data point from N4 (SIGKILL of the launcher) stays a
deliberate follow-up on the launcher family, unchanged.

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

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

* fix(managed-agent): Gate the POSIX-arm writer case on the host, not the flag

The round-5 review found the split's new case reproducing R4-1's exact
failure on the Windows lane one level up: 'writes the env content and no
ps1 sibling on the POSIX arm' injected isWinPlatform: false unguarded, so
on a real Windows runner the production 0700 gate reads the synthesized
777 directory mode and writeSpringEnvFiles throws before either
assertion — the mechanism and message R4-1 described, on the case I
believed the split had made portable. It now gates on the host like its
six siblings.

Also adopts the round-3 verification's wording note: the two sibling
installs are labeled once per clone, and re-run after pulling changes
to qwencode or runtime-broker, since a snapshot install before a main
pull leaves spring-boot:run compiling against stale artifacts (the
banner builder and the README recipe say the same thing either way).

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

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

* fix(managed-agent): Close the actionable slice of the round-4 deferred list

Five items from the round-4 review body's deferred Suggestions were
covered here because each is a small consistency hole in the seam this
PR just built:

- main() re-checks pinned ports at t0 before writeSpringEnvFiles runs,
  so a busy --daemon-port/--harness-port no longer rotates credentials
  and prints the banner before failing at stage spawn the same run
  (R4-14); the per-stage recheck stays for the boot window it was added
  for.
- launchWebShell re-consults the teardown flag after resolveWebPort, so
  a launcher dying mid-probe no longer prints a Managed URL for a group
  the exit hook is about to reap (R4-10), with a witness that flips the
  flag mid-resolve.
- findWebPort is now held against a stand-in bound to this host's
  localhost resolution at DEFAULT_WEB_PORT, so the family walk is
  operational rather than vacuous (R4-17).
- installTeardownHandlers' captured handlers are asserted to be
  functions, not only named functions (R4-19).
- The vi.mock child-process fixture spawns fake children with
  pid=undefined, so teardown code can never signal a real process
  through the registry it is pushed into (R4-20).

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

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-10-04 14:41:46 +00:00
destire-mio
612a552959
feat(channels): add IMAP and SMTP email channel (#12939)
* feat(channels): add IMAP and SMTP email channel

* test(channels): include email in managed channel catalog expectations

---------

Co-authored-by: destire-mio <248462155+destire-mio@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-10-03 02:54:25 +00:00
易良
354ae9292d
feat(core): resolve model limits and modalities from a models.dev catalog (#11959)
* feat(core): resolve model limits and modalities from a models.dev catalog

Bundle a trimmed models.dev snapshot (12 first-party providers, 237
models) and refresh it in the background once a day into
~/.qwen/model-registry.json with ETag revalidation, so a model that
appears on models.dev gets its context window, output limit, and input
modalities without a CLI release. tokenLimit()/knownTokenLimit()/
hasExplicitOutputLimit() consult the catalog before the regex tables;
defaultModalities() merges both so the catalog can only add modalities.

QWEN_CODE_MODELS_DEV=off restores the regex-only behavior (default in the
unit test setups), QWEN_CODE_MODELS_DEV_REFRESH=off disables the network
refresh, QWEN_CODE_MODELS_DEV_URL points at a mirror. `npm run
generate:model-catalog` regenerates the committed snapshot.

Implements PR2 and PR3 of the design in #9851. Refs #8558.

* feat(core): add model.customCatalog for offline model metadata

Offline and proxy-restricted users cannot reach models.dev, so let them
point `model.customCatalog` at a URL or local file whose entries are
merged over the bundled catalog per model and field. Both the trimmed
`{ models: { id: entry } }` shape and models.dev-style payloads are
accepted. A file is re-read synchronously at every start so the first
model resolution already sees it; a URL is cached like models.dev and
is still fetched when QWEN_CODE_MODELS_DEV_REFRESH=off. The cache
records its source, so a changed or removed setting drops the overlay.

Also sort snapshot keys with a plain comparator instead of localeCompare
so regeneration is deterministic across machines.

* docs: add a verification brief for the models.dev catalog

CI covers the mechanism, but every unit test stubs fetch and the test
setups run with the catalog off, so neither the data values nor the real
runtime behavior are verified anywhere. This brief is the handoff for a
machine that can run the CLI against live endpoints: an on/off A/B with
measured reference numbers, the refresh and offline paths, and the two
values that may still be wrong (Anthropic 1M context, qwen3.8-max PDF).

* fix(core): only catalog models the endpoint question has one answer for

Two projection bugs, both found by reviewing the magnitude of the changes
the catalog was making rather than their direction.

The provider whitelist filtered by vendor but never by model type, so the
catalog also carried embedding, text-to-speech, image, video, OCR and
translation models whose limits mean something else entirely:
gemini-embedding-001 reported an output limit of 1 and veo-3.1-generate a
context of 480, and those numbers outranked the family fallbacks that keep
such ids harmless today. Require tool calling and text output, which drops
34 ids and no real chat model.

Conflict resolution picked the vendor's numbers when several providers
served one id, but a context window is a property of the endpoint, not the
weights. DashScope caps GLM-5 output at 16,384 where Z.ai allows 131,072;
both are correct for their own endpoint. Worse, kimi-k2.6 has no matching
output pattern today, so it is treated as unknown and the user's max_tokens
is respected, and the catalog would have clamped it to 262,144. Drop such
ids instead of guessing: 220 of 237 ids have no conflict, and the 11 that
remain keep exactly the answer they get today.

Snapshot goes from 237 models to 192, 29,824 to 24,685 bytes.

* fix(core): validate catalog defaults and reject alias conflicts

* fix(core): narrow catalog defaults to verified behavior

* fix(core): preserve curated output limits over catalog defaults

* fix(core): harden models.dev catalog refresh and keying

- Reject a 200 response that projects to zero models so a gateway error
  body or an upstream field rename cannot shadow the bundled snapshot,
  and treat an already-empty cache as absent so a 304 cannot re-stamp it.
- Omit catalog candidates whose key is not a normalize() fixed point:
  deepseek-v3 was unreachable by its own name while its dated alias
  inherited a 163,840 output limit. Snapshot drops that entry (194).
- Exclude QWEN_CODE_MODELS_DEV_URL from project .env files: the refresh
  writes the global cache every project reads.
- Reject false-valued modalities when parsing catalog caches.
- Document the fixed-point keying rule and the accurate REFRESH=off
  semantics in the settings table.

* fix(core): preserve explicit budgets for catalog-only models

* fix(core): harden catalog cache reads and correct qwen3-coder-plus window

- loadModelCatalog drops cache keys no normalized spelling can reach and
  treats a cache that is empty after validation as absent, so a stale or
  poisoned cache can no longer displace the bundled snapshot.
- Correct qwen3-coder-plus to its vendor-declared 1,000,000-token window
  (models.dev's alibaba buckets approximate it as 1,048,576), applied in
  the loader so a refreshed cache cannot reintroduce the approximation.
- Pin the cross-origin refresh guard with a non-empty cache fixture, the
  normalized-id catalog lookup for modalities, the env-budget clamp
  precondition for glm-4.7, and control keys plus reload/snapshot arms
  for the project-scoped catalog URL exclusion.
- Align the QWEN_CODE_MODELS_DEV_REFRESH docs and JSDoc with the
  newer-than-bundled selection rule.

* fix(core): project catalog modalities across providers and test against the real catalog

Review round on PR #11959:

- trimModelsDevCatalog: token limits stay gated on the first-party
  allowlist with disagreement-drop; input modalities describe the
  weights, so they now union across every provider that serves the
  model. First-party vendors outside the allowlist (issue #8558's
  thinkingmachines/inkling) project image/pdf support again, while
  their endpoint limits stay untrusted.
- modalityDefaults.test.ts: drop the file-scoped catalog mock that let
  three assertions pin the opposite of shipped behavior; run against
  the real bundled snapshot in an isolated QWEN_HOME and correct the
  qwen3.8-max / glm-5v-turbo / glm-5.3-flash expectations. The
  mock-based merge tests move to modalityDefaults.catalog.test.ts.
- vscode-ide-companion: add the missing QWEN_CODE_MODELS_DEV=off test
  default via test-setup.ts so a host's ~/.qwen/model-registry.json
  cache cannot flip its contextLimit assertions.

* revert(vscode-ide-companion): drop out-of-footprint catalog test wiring

Deterministic verification rejected the setupFiles addition to
packages/vscode-ide-companion/vitest.config.ts: per-package vitest
configs are test-config verification machinery, and this PR's
footprint does not include that package. Revert the wiring and remove
the now-orphaned test-setup.ts.

The underlying hazard (R4-2: acpModelInfo contextLimit assertions
resolve against the host's ~/.qwen/model-registry.json cache) is real
and reproduced on this head — a poisoned cache flips exactly the two
witness tests red — but every fix lives outside this PR's footprint,
so it is deferred to the follow-up queue with an open question for
maintainers.

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

* fix: close model catalog environment gaps

* fix(core): validate refreshed model metadata

* fix(core): reject incomplete model catalogs

* fix(core): keep catalog defaults agent-safe

* fix(core): preserve DeepSeek vision boundary

* fix(core): harden the catalog refresh fetch and pin the catalog fixture clock

Six review findings on the refresh/generator path:

- Bound the download: the refresh now reads the 200 body with a
  200KB ceiling (the generator's budget) instead of buffering an
  unbounded payload, rejects a present-but-non-JSON Content-Type one
  step earlier than JSON.parse, and cancels the body on the HTTP-error
  path so the socket is released.
- Clock skew: a cache stamped by a faster clock (negative or unreadable
  age) now refetches instead of serving indefinitely; the re-stamp uses
  the local clock so the skew self-heals.
- The refresh failure log goes through the canonical getErrorMessage,
  so error.cause survives into the debug channel.
- The 'serves it immediately' test pins its clock ahead of the bundled
  snapshot's committed fetchedAt, decoupling it from the runner's wall
  clock (the snapshot is regenerated by whichever host runs the
  generator).
- The generator refuses a non-OK response and an empty projection
  instead of overwriting the committed snapshot with either.

Each new test is red under its corresponding revert (byte cap, content
-type check, skew guard); the clock-pin witness mutates the snapshot's
fetchedAt to 9999 and stays green.

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

* fix(core): drop mirror providers from catalog limits and keep parse fidelity

Seven more review findings on the catalog path:

- MODELS_DEV_PROVIDERS no longer includes modelscope. Measured against
  the live payload: its only contributions were four dated -2507 qwen3
  snapshots recorded under the bare ids plus vetoing z.ai's first-party
  glm-4.6 numbers into a conflict drop. volcengine stays — it is the
  first-party endpoint for the doubao/seed family (dropping it costs 16
  keys). Snapshot regenerated: 203 entries, glm-4.6 restored to the
  first-party figures.
- parseModelCatalog no longer lets one bad modalities member take a
  whole entry down: unknown or mistyped modality keys are filtered and
  the entry keeps its limits; context/output stay strict. The parser
  also skips an own-property __proto__ key instead of invoking the
  prototype setter (the refresh path can legitimately write one).
- A declared text-only modalities list now survives projection as {}
  on entries that ship limits, instead of being indistinguishable from
  "upstream said nothing" — inert under today's union, recorded so a
  future narrowing can tell the two apart. A bare {} never creates an
  entry on its own, so conflict-dropped and limit-less models still
  drop.
- The committed snapshot now has a parse-and-budget gate: every shipped
  entry must survive parseModelCatalog and stay within the 200KB budget
  (corrupting one entry turns the new test red).
- The gpt-4o resolver expectation's comment now says it pins the
  catalog-off regex answer; the catalog-on production value already has
  its own twin test.
- The verification README's evidence pointers no longer reference a
  gitignored .qwen path; they name the committed suites that pin the
  same numbers.

Modality list is now single-sourced as CATALOG_MODALITIES exported from
model-catalog.ts. Each behavior change is red under its revert:
restoring modelscope re-drops glm-4.6, restoring whole-entry modality
rejection or the __proto__ assignment reds the parse test, restoring
the empty-modalities drop reds the projection expectations.

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

* fix(core): narrow catalog modality keys with a type predicate

Object.entries widens the key to string, and includes() on the widened
readonly string[] does not narrow it back, so the indexed write into
InputModalities was an implicit any under noImplicitAny. That failed the
packages/core build with TS7053 and cascaded into every required check.

Extract the membership test into an isCatalogModality predicate so the
write is type-safe. Type-level only, no behavior change: the existing
"drops malformed entries while parsing" coverage still pins that an
unrecognised modality key degrades that one field instead of discarding
the entry.

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

* fix(core): keep catalog refresh usable and process-consistent

* fix(core): invalidate incompatible model catalog projections

* fix(core): stamp canonical models.dev URL as bundled catalog provenance

The generator passed its input locator straight through as the provenance
stamp, so generating from a local payload copy recorded a machine-specific
path (e.g. /tmp/11959-models-dev.json) in the committed snapshot. Stamp
MODELS_DEV_URL for local-path inputs (the payload is models.dev data
regardless of where the machine stored it), keep URL inputs stamping
themselves, regenerate the snapshot, and pin the artifact's source field
with a test.

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

* fix(core): share the provider-coverage guard and key DeepSeek narrowing on the family root

Round-9 review findings on 4516bd759d:

- R9-1: the snapshot generator only checked "at least one entry
  projected", while the runtime refresh treats a provider-incomplete
  payload as fatal. A truncated download or a mirror missing one vendor
  would silently replace the committed 203-entry snapshot. Extract the
  coverage check into assertCatalogPayloadComplete() and call it from
  both refreshRemote and scripts/generate-model-catalog.ts.

- R9-2: the #10270 DeepSeek text-only narrowing keyed on the hyphenated
  prefix `deepseek-`, but normalize() folds bare-model records onto the
  family root (`deepseek-v3` -> `deepseek`), which passes
  isModelCatalogKey — so a bare record kept an overstated image claim.
  Key the guard on the family: /^deepseek(?:-|$)/.

Both pins are mutation-verified: reverting either change turns its new
assertion red.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-issue-patrol/jmuolga6k0g

* fix(core): keep four more catalog windows at their declared value

models.dev buckets the vendor-declared window of kimi-k3 and
qwen3-coder-plus up to 1,048,576, and MiniMax-M2.5 / GLM-4.7 up to
204,800, while the curated table in tokenLimits.ts and the provider
presets both declare the real value. The catalog wins for input limits,
so for kimi-k3 the compaction thresholds computed from the inflated
window fired above the window the vendor accepts
(computeThresholds(1048576).hard = 1,025,576 > 1,000,000).

The correction list already had the shape for qwen3-coder-plus; extend
it to the complete set of ids where the catalog contradicts both a
curated row and a preset, and drive the application from one table.

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

* fix(cli): freeze all three model-catalog keys against project config

A project `.env` or `settings.json` `env` block could set
QWEN_CODE_MODELS_DEV and QWEN_CODE_MODELS_DEV_REFRESH even though
QWEN_CODE_MODELS_DEV_URL was already excluded. On the `qwen serve` fast
path both keys are applied before runQwenServeImpl snapshots and freezes
process.env into daemonRuntimeBaseEnv, which every workspace session
child inherits and which setRuntimeEnvIfUnset cannot repair per
workspace — so one repository's config decided catalog behaviour for
every other workspace the daemon hosts, and the rejection was silent
because the keys are not loader keys. On the reload path a repository's
`on` force-wrote over an operator's exported `off`, re-enabling the
outbound refresh in an air-gapped environment.

Add both keys beside the URL key in PROJECT_ENV_HARDCODED_EXCLUSIONS
and document the project-file restriction on their settings.md rows.

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

---------

Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Qwen Autofix Bot <autofix@qwen-code.dev>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: yiliang114 <yiliang114@users.noreply.github.com>
2026-10-02 14:39:11 +00:00
qqqys
e767e223c5
fix(hooks): deliver pre-tool and ACP failure context to the model (#13006)
* fix(hooks): deliver pre-tool and ACP failure context to the model

PreToolUse additionalContext was dropped on the deny/ask/stop branches of
the hook helper and never attached to the tool result in core or ACP, and
ACP only logged PostToolUseFailure context on both the returned-error and
thrown-error paths.

Append the sanitized context to the owning call's functionResponse:
- core keeps it per call across an ask bounce and assembles it after
  PostToolBatch, before the final batch budget; cancelled calls drop it
- ACP appends Pre and failure context to model-facing parts only, keeping
  UI updates and error projections unchanged
- the added text is bounded per call by truncateToolOutputThreshold

* fix(hooks): keep hook context out of nested and App results

- exec: a nested call's result is a value for the script, so its hook
  context is handed to the parent exec and delivered with that result
  (core via the nested scheduler, ACP via the nested run result)
- ACP MCP App calls have no model consumer and never append context
- core rechecks each call's abort signal at final assembly, so a turn
  cancelled during PostToolBatch drops the stored context
- share the functionResponse text append with PostToolBatch context
- run the hook-context integration test in the no-AK PR lane

* fix(hooks): route nested failure context and pin review cases

- core: a code-mode nested call's PostToolUseFailure context goes to the
  parent exec result instead of the error handed to the script, matching
  ACP and nested PreToolUse context
- ACP: nested hook context keeps dispatch order under concurrent nested
  calls, so truncation does not depend on completion order
- docs: scope delivery to Code Mode nested calls, the turn-cancel
  exception, the per-surface limits, and transcript/detail visibility
- tests: sandbox-safe decision file, self-delimiting markers and a real
  value check in the integration test, ACP agent-exit handling, plus
  ACP cancel/truncation/nested-failure and core contentLength and
  nested-failure regressions

* fix(hooks): drop hook context when the turn is cancelled before recording

- core: after the final output budget (which can await persistence),
  redo the budget without the context of any call whose turn was aborted
- ACP: queued records keep their parts without hook context; if the turn
  is aborted before finalizeAndRecord records them (e.g. while notifying
  the client or persisting), finalize and record those parts instead
- tests: cancellation during the final budget (core) and while notifying
  a completed or denied result (ACP); the ACP integration agent gets its
  own QWEN_HOME and is awaited before teardown

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-09-30 06:50:21 +00:00
Bob.qwencode
34dbaf6741
chore(release): v0.24.7 (#13025)
* chore(release): v0.24.7

* docs(changelog): sync for v0.24.7

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-29 14:36:56 +00:00
destire-mio
cc45337481
fix(cli): report completion for saved workflow slash commands (#12415)
* fix(cli): report completion for saved workflow slash commands

* test(cli): run workflow completion regression in PR CI

* fix(cli): preserve foreground workflow completion delivery

* fix(cli): refresh workflow progress after cancellation

* fix(cli): address workflow completion review feedback

* fix(workflow): preserve bounded completion details

Pass storage snapshot destinations into completion notices and keep reported failures outside the result preview. Share serialization and Unicode-safe XML budgeting with tool cards and sub-session notifications.

Clarify Ink and OpenTUI guidance and cover truncation, terminal controls, and throwing failure getters.

Validation: 594 focused unit tests, 4 interactive workflow cases, build and bundle, workspace and integration typechecks, and changed-file lint and formatting checks.

* fix(workflow): preserve error details in completion reports

* fix(workflow): retain collection failures and concise error reasons

* fix(workflow): qualify truncated result snapshot notices

---------

Co-authored-by: destire-mio <248462155+destire-mio@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-09-27 03:30:09 +00:00
Shaojin Wen
c7c6def8bc
test(managed-agent): add Hosted no-tool process gates (#12733)
* test(managed-agent): add Hosted no-tool process gates

Refs #12728

* fix(ci): satisfy YAML quoting rules in hosted process gates
2026-09-26 08:33:27 +00:00
Bob.qwencode
16496a71ec
chore(release): v0.24.6 (#12722)
* chore(release): v0.24.6

* docs(changelog): sync for v0.24.6

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-26 01:06:56 +00:00
jinye
34246c6606
feat(managed-agent): Spring control plane and dual-path WebShell (#12692)
* feat(managed-agent): add Spring control plane and dual-path WebShell

Bring the standalone managed-agent server, its embedded broker listener,
and the WebShell Java panel onto the current broker contract. Legacy
daemon sessions stay available beside the new path.

Co-authored-by: jinye <doudouOUC@users.noreply.github.com>

* fix(managed-agent): address review defects and wire server CI

* fix(managed-agent): preserve case-sensitive store identities

* fix(managed-agent): bind locally and return 404 for missing routes

* codex: address PR review feedback (#12692)

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: jinye <doudouOUC@users.noreply.github.com>
Co-authored-by: jinye.djy <jinye.djy@U-Y37CF7G3-1943.local>
2026-09-25 16:30:32 +00:00
易良
73aa65a4b4
chore(desktop): rename packages/desktop-shell to packages/desktop (#12653)
* chore(desktop): rename packages/desktop-shell to packages/desktop

The Electron-era packages/desktop was removed in #9085 and the OpenWork
downstream has forked, so the short name is free and the shell suffix is
the only thing left of the transition. Rename the package directory and
every load-bearing reference: workspace negations (npm + pnpm), the ci.yml
crate filter, desktop-release.yml, the isolation guard, lint/ignore configs,
scripts tests, CLI workspace-fixture tests, and living docs.

User-facing identity is untouched: the crate stays qwen-code-desktop,
productName stays "Qwen Code Desktop", the bundle identifier, updater
feed, release tag scheme, and the legacy electron-bridge asset names are
all unchanged. check-desktop-isolation now guards both path names so the
old one cannot re-enter a workspace set. Dated design docs under
docs/design/ keep their historical paths deliberately.

Refs #8596

* docs(design): follow the rename in the undated notification design notes

The web-shell-browser-notification-details pair is a living doc without a
date prefix, so it falls under the PR's update rule rather than the
historical-record exception.

* docs(desktop): stop citing paths the rename invalidated

Three prose sites and one CI comment still described the pre-rename world,
or contradicted themselves once `packages/desktop` named both the removed
Electron app and the living Tauri shell.

- ci.yml `desktop_shell`: the comment this branch added credited the
  Cargo.toml guard with keeping "branches from either side of the rename"
  green. It does not. A pre-rename head's files sit under
  `packages/desktop-shell`, which the changed-files filter no longer
  matches, so the job skips before the guard ever runs. Say that, and say
  what the guard does cover (a head the filter matched because ci.yml
  itself changed, with no crate at the new path).
- docs/design/web-shell-user-message-links.md: "desktop-shell routing" /
  "desktop-shell clicks" -> "desktop-host", the wording this branch already
  chose in LinkifiedText.tsx for the same rule.
- docs/design/telemetry-runtime-client-attribution-design.md: repoint the
  `runtime.rs` citation. It backs the live `QWEN_CODE_DESKTOP=1` claim, and
  the other document of record for that invariant -- the comment in
  acp-channel-fallback.ts -- was already repointed by this branch, so the
  two disagreed. Only the path token changes; the doc's dated findings are
  untouched.
- brand-create.mjs / SKILL.md / src-tauri/src/main.rs: name the historical
  referent as historical (the Electron `packages/desktop` removed in PR
  9085) instead of letting it read as a self-reference. In main.rs the
  citation `packages/desktop/packages/shared/src/config/storage.ts` now
  looked like a subpath of the file's own package. Rewritten, not deleted:
  the `QWEN_DEFAULT_WORKSPACE_DIR` clause next to it is the only in-code
  documentation of that override and still resolves.

Deliberately not swept: `scripts/check-desktop-isolation.js`'s
`packages/desktop-shell` tripwire, ci.yml's #8132 history comment,
`docs/design/9152-architecture-invariant-classification.md` (names both
spellings and is accurate), and the date-prefixed
`docs/design/2026-07-31-desktop-web-shell-release.md`.

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

* test(scripts): pin the desktop crate path and the isolation guard's coverage

Both invariants fail green today, and this rename is the second time the
crate directory has moved.

`scripts/check-desktop-isolation.js` gained `'packages/desktop'` in
`nativePrefixes` -- the only production-logic line in this branch -- and
nothing read it. Deleting that line leaves the guard's output and exit code
byte-identical, because `isNativeLocation('packages/desktop')` then matches
no remaining prefix; a later PR that drops `!packages/desktop` from the root
workspace list would re-admit the package with CI green.

The `desktop_shell` job names the crate directory in five places (the
changed-files filter, the Cargo.toml guard and its `::notice::` text, the
rust-cache `workspaces:`, and two `working-directory:` values). Nothing
asserted they agree with each other or with a directory that exists, so the
next drift reports a skipped job as a passed one.

The new suite parses both instead of re-running them, and is superset-shaped
on purpose: `nativePrefixes` carries a fourth entry (the
`packages/desktop-shell` tripwire) that nothing negates, and an exact-match
assertion would invite deleting it. The npm/pnpm mirror of the negation list
is already pinned by package-scripts.test.js, so this reads package.json only.

Verified by mutation, each applied alone and then restored: reverting the
filter alternative or either `working-directory` to `packages/desktop-shell`,
dropping `'packages/desktop'` from `nativePrefixes`, and dropping
`!packages/desktop` from package.json all turn this suite red. Today no other
test does.

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

* test(scripts): pin what the desktop-isolation tests actually claim

Four review findings on scripts/tests/desktop-isolation.test.js, each
verified by mutating the thing it claims to pin, before and after.

- The comment above the local `isNativeLocation` copy claimed a change to
  the script's rule "has to be made here too". Nothing reads the rule back
  from the producer -- `nativePrefixesFromSource` parses the array literal
  only -- and narrowing the script's rule to prefix equality leaves the
  suite green. Describe the copy instead of claiming coupling that does not
  exist.
- `still carries the Cargo.toml existence guard` matched only the guard's
  condition text. Flip the body's `changed=false` to `changed=true`, or move
  it after `fi`, and the suite stayed green at 5/5 while the job runs every
  gated step against a crate that is not there. Assert the assignment sits
  inside the guard's own `if ... fi` body.
- Both on-disk checks keyed on the bare directory, so git-ignored residue
  under `packages/desktop-shell` (`node_modules`, which survives a branch
  switch with an empty `git status`) reddened two tests on a tree with no
  defect and blamed the root workspace set. Key on `package.json`, which is
  what `packages/*` requires to make a directory a workspace member; a real
  re-entry still reddens both.
- The `startsWith(filterAlternative)` assertion compared a path built from
  `crateDir` -- `filterAlternative` with one trailing slash stripped -- back
  against `filterAlternative`, so it could not fail for any input. Dropping
  the trailing slash from the changed-files filter in ci.yml left it green.
  Replace it with the property that makes the alternative a path prefix
  rather than a bare string prefix.

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-25 05:41:25 +00:00
易良
010941d202
fix(standalone): pin @lydell/node-pty-linux-arm64 and fail release on missing prebuilds (#12649)
* fix(standalone): pin @lydell/node-pty-linux-arm64 and make missing prebuilds fatal at release

The wrapper publishes a linux-arm64 prebuild at the same 1.2.0-beta.10 pin,
but the root and core manifests never declared it, so the linux-arm64
standalone archive and the linux-arm64 Desktop runtime both degraded to
"PTY not available" while every other target shipped a working web
terminal.

With the pin in place every shipped target resolves a prebuild package, so
the release archive build can stop degrading silently: release.yml now
exports QWEN_STANDALONE_REQUIRE_NODE_PTY_PREBUILD=1 next to the existing
audio-capture gate, turning a missing prebuild into a hard failure instead
of another #11872.

Fixes #11872

* fix(standalone): update pnpm lockfile for the linux-arm64 node-pty pin

The two package.json manifests gained @lydell/node-pty-linux-arm64 but
pnpm-lock.yaml was not regenerated, so every CI job failed at the
frozen-lockfile install with ERR_PNPM_OUTDATED_LOCKFILE.

* docs(standalone): align the linux-arm64 PTY guide and tripwire with the pin

Pinning @lydell/node-pty-linux-arm64 makes linux-arm64 a supported PTY target,
but INSTALLATION_GUIDE.md still told users that archive ships without PTY and
reports "PTY not available". CI could not catch the drift because
install-script.test.js pinned that exact sentence as its tripwire, so the guide
and the assertion have to move together.

List all five shipped targets in the guide, retarget the tripwire at the new
sentence, and refresh the matching stale comment in desktop-shell's
test-release.js degrade fixture - that arm now only fires for a target the map
does not know.

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

* fix(standalone): complete the node-pty linux-arm64 pin in esbuild externals

`create-standalone-package.js` documents that "esbuild.config.js keeps every
node-pty specifier external" so the bundled CLI resolves the platform prebuild
from `lib/node_modules` at runtime. The external list carried five of the six
pinned platform packages and omitted `@lydell/node-pty-linux-arm64` — the exact
platform this PR pins. Nothing imports that specifier statically today, so the
bundle is unaffected; the gap is that the invariant the archive's PTY support
rests on was one entry short of complete.

Probe (esbuild, `--platform=node --format=esm`, entry importing both
specifiers):

- without the entry: `Could not resolve "@lydell/node-pty-linux-arm64"`
- with the entry: builds clean and emits the import left external, matching
  the other five platforms

Also corrects two comments this PR rewrote in the previous round. Both claimed
the desktop degrade arm "fires only for a target the map does not know (a
future platform)". That trigger is unreachable: `desktopTarget()` throws
`Unsupported desktop target` for anything outside its five-entry allowlist and
`NODE_PTY_PREBUILD_PACKAGE` maps exactly those five, so `prebuildPackage` is
always a real name. Verified by running the script:

    $ QWEN_DESKTOP_SKIP_BUILD=1 QWEN_DESKTOP_TARGET=freebsd-x64 \
        node packages/desktop-shell/scripts/prepare-runtime.js
    Error: Unsupported desktop target: freebsd-x64
        at desktopTarget (prepare-runtime.js:351:11)

The reachable trigger is the one the warn string already names and the one
`test-release.js` reproduces four lines below its own comment: a known target
whose prebuild package is unpinned in the source root manifest.

Comment-only apart from the single external entry. prettier --check and
eslint --max-warnings 0 clean on all three files; node --check passes.

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

* test(standalone): pin the node-pty release gate and its target map

Closes out three review findings on the node-pty pin. Test- and
comment-only apart from one new export; no production behavior changes.

R1-1 prepare-runtime.js: the nodePtyPackageSpecs() docstring still claimed
its null return "is how an unsupported target degrades". desktopTarget()
throws for anything outside its five-entry allowlist, and
test-release.js:testRuntimeNodePtyTargetMapping() asserts
NODE_PTY_PREBUILD_PACKAGE's keys equal exactly that allowlist, so an
unknown target can never reach it. A null here is a dropped pin. Verified
by re-running that tripwire's extraction against the edited file: both
literals still match and keys == allowlist == the same five targets.

R1-2 create-standalone-package.js: export TARGET_PREBUILD_DIR and assert
every release target maps to a package the manifests actually pin. The
guide sentence this PR adds states that invariant while release.yml:707
makes violating it abort the whole archive step, yet the map had zero test
references. The expectation is derived from readNodePtyPackageSpecs() so
the version list stays single-sourced, and is one-directional because
@lydell/node-pty-win32-arm64 is pinned while no target maps to it.

R1-3 install-script.test.js: the release.yml assertion matched only the
variable name, which also appears in the packager's usage text, so nothing
connected producer to consumer and no test executed the fail() branch at
create-standalone-package.js:578. Tightened to the full wiring line and
added the behavioral twin of the audio-capture pair: gate on with the
addon removed must throw, gate off must still build a PTY-less archive.
packageFakeStandalone() gained an env option for this; spreading
process.env leaves its 15 existing callers unchanged.

Mutation-checked, each mutant applied alone then restored and md5
verified: win-x64 mapped to an unpinned name, gate reader renamed to '2',
gate polarity flipped to !==, and release.yml no longer evaluating to '1'
are each caught by a new or tightened assertion. The polarity flip was
previously undetectable on the PR lane, where the linux-x64 prebuild is
present so the gate is never reached.

Focused: install-script.test.js 130 passed / 11 skipped (141).
prettier --check and eslint --max-warnings 0 clean on all three files.

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

* docs(standalone): state the real node-pty degrade conditions in comments

prepare-runtime.js said the degrade arm fires only "where the repo pins
nothing", but nodePtyPackageSpecs returns null at the first unpinned name of
the pair, so it also fires when the wrapper pin is the one that went missing
while the warning names the prebuild package. Say "either name in the pair"
and note which package the warning names.

create-standalone-package.js justified the non-fatal default with "a developer
archive stays usable on an unmapped future target", which cannot happen:
main() calls copyNativeAddon first and it reads the same TARGET_PREBUILD_DIR
in an unconditional path.join(), so an unmapped target dies with a TypeError
in the audio step before copyNodePtyAddon runs. Replace the rationale with the
case that does reach the warn arm (a target whose prebuild the host install
does not have).

Comment-only; no behaviour change.

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

* test(standalone): make the node-pty gate assertions discriminate

Pin the `Required ` prefix on the fatal-gate regex. copyNodePtyAddon builds
one message and emits it on both paths, so the prefix-less regex was also
satisfied by the degrade warning folded into the child's stderr — the suite
would have reported the gate as covered after the prefix was dropped from
fail(). The sibling clipboard gate assertion already spells the prefix.

Assert QWEN_STANDALONE_REQUIRE_NODE_PTY_PREBUILD structurally on the `Build
Standalone Archives` step env instead of as a whole-file substring, so
commenting the line out or moving it into a neighbouring step's env goes red.
build-standalone-release.js spawns the packager with no env override, so that
step's env is the only route to the gate. The comment defending the substring
pin named a collision with create-standalone-package.js's usage text, which
cannot occur: the searched string is release.yml alone.

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

* test(standalone): cover the node-pty target map and the win-x64 dir name

Two coverage gaps left by the previous round of gate tests:

- The map test looped over TARGET_PREBUILD_DIR itself, so a release target
  with no map entry was never visited, deleting a whole entry merely removed
  an iteration, and an emptied map passed vacuously. Drive the loop from
  TARGETS and assert the lookup before the pin. Still one-directional:
  @lydell/node-pty-win32-arm64 is pinned while no target maps to it.

- Both gate tests reached the packager through packageFakeStandalone, which
  hardcoded --target linux-x64, the one dialect where the prebuild dir name
  equals the target name. Add a win-x64 arm so the 'win-x64' -> 'win32-x64'
  translation in copyNodePtyAddon is covered on every lane rather than only
  on test_windows. Parameterise the staged-modules fixture and the packaging
  helper to make that reachable without a Windows host.

Test-only change; no production code is modified.

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-25 04:38:07 +00:00
Bob.qwencode
af5e90bfb2
chore(release): v0.24.5 (#12661)
* chore(release): v0.24.5

* docs(changelog): sync for v0.24.5

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-24 17:19:31 +00:00
Bob.qwencode
1b26d38b5c
chore(release): v0.24.4 (#12476)
* chore(release): v0.24.4

* docs(changelog): sync for v0.24.4

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-22 15:19:32 +00:00
易良
baf0ff41f7
refactor(build,release): Use pnpm workspace builds and publishing (#12400)
* refactor(release): Use pnpm for workspace publishing and versioning

* fix(ci): revert desktop-release install to npm ci for Windows compatibility

The pnpm migration added a bash conditional to check for pnpm-lock.yaml,
but the step did not specify shell: 'bash'. Windows runners default to
PowerShell, which cannot parse bash syntax, causing the build to fail
with a ParserError.

Since desktop-release always checks out the same commit and the root
lockfile is now pnpm-lock.yaml, the conditional is unnecessary. Revert
to the simple npm ci command that works on all platforms.

Fixes the Windows build failure in Desktop release v0.24.2.

* refactor(build): Schedule workspace builds with pnpm

* fix(ci): Preserve Windows release installation and version filters

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-21 15:38:36 +00:00
Bob.qwencode
364032a032
chore(release): v0.24.3 (#12418)
* chore(release): v0.24.3

* docs(changelog): sync for v0.24.3

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-21 14:46:57 +00:00
Bob.qwencode
eceaede18e
chore(release): v0.24.2 (#12359)
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
pnpm Worktree Smoke / Install (macos-latest) (push) Waiting to run
pnpm Worktree Smoke / Install (ubuntu-latest) (push) Waiting to run
pnpm Worktree Smoke / Install (windows-latest) (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
* chore(release): v0.24.2

* docs(changelog): sync for v0.24.2

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-20 16:05:04 +00:00
易良
64dd058c9c
ci(pnpm): install with pnpm everywhere and retire package-lock.json (#11859)
* ci(pnpm): install dependencies with pnpm in CI and release

CI tests the graph release ships only if both install it the same way, so
this moves both, keeping every `npm run` script and `npm publish` as they are.

- ci.yml (7), release.yml (3) and release-vscode-companion.yml (2) install
  with `corepack pnpm install --frozen-lockfile`; release keeps
  --ignore-scripts plus its explicit postinstall and generate steps.
- setup-node drops the npm cache and sets package-manager-cache: false,
  which setup-node v5+ needs once package.json declares a pnpm
  packageManager.
- A composite action caches the pnpm store on the three always-hosted jobs;
  the ECS pool keeps its store on local disk.
- version.js no longer lets npm reify node_modules: --no-workspaces-update on
  both `npm version` calls, and a package-lock-only refresh in step 9.
- The notices generator, prepare-package's sharp pin and the standalone
  release's native package specs read the installed tree or pnpm-lock.yaml
  instead of package-lock.json keys.
- New size baselines for the three workflows, which were already over them.

* chore(vscode): regenerate NOTICES from the pnpm-installed tree

Generated in CI (pnpm Worktree Smoke on ubuntu-latest) by the ported
generator. Same entry set as before except the five packages npm and pnpm
resolve differently: fdir 6.5.0, picomatch 4.0.5, mdurl 2.1.0,
@types/node 22.20.1 and mime-db 1.52.0 (form-data's pinned copy). No entry
lacks a license text or repository.

* fix(pnpm): resolve undeclared zod peers to the repo's zod, as npm does

The first CI run on this branch failed the export renderer's size budget:
1,930,015 bytes against 1,930,000, where the same commit built on npm's
tree comes to 1,858,186. Comparing both trees' esbuild metafiles and
prebuilt bundles put the whole difference in web-shell's transcript.js
(+100,903 bytes pre-minify), and the added code is zod 4 plus the
@modelcontextprotocol/ext-apps schemas; the sdk daemon bundle is identical.

web-shell bundles ext-apps, whose zod and MCP SDK are peers, and declares
no zod itself, so pnpm filled the peer with the highest locked zod (4.4.3)
while npm serves the hoisted 3.25.76. Declaring zod 3.25.76 as a root
devDependency makes pnpm resolve every such peer to it
(resolve-peers-from-workspace-root is on by default), which is npm's
hoisting behaviour. mobile-mcp keeps its own zod 4.4.3, as under npm; the
npm tree does not change.

* fix(pnpm): keep npm's root @types/node so root-level typechecks match

The no-AK integration gate typechecks integration-tests/, which takes
Node's types from the root node_modules. npm hoists @types/node 20.19.1
there, because acp-bridge and core pin it; pnpm, with no root declaration,
hoisted 22.20.1, whose spawnSync overloads type process-registry.ts's
stdout as string | NonSharedBuffer and fail TS2322. The same code passes on
every npm-installed PR.

Declaring 20.19.1 as a root devDependency puts npm's copy back where a
root-level tsconfig looks. The npm tree does not change.

* ci(pnpm): install with pnpm in the remaining workflows

The follow-up to switching CI and release: every other workflow that
installs the root workspace now uses the pinned pnpm with
--frozen-lockfile, so no job tests a graph that release does not ship.

- e2e, tui-parity, repo-hygiene, web-shell-visuals, serve-ab, sdk-java,
  windows-runner-smoke, qwen-autofix, the SDK releases, finalize-release,
  sync-release-to-oss, the root install in desktop-release and
  cd-cua-driver, and the sandbox Dockerfile.
- setup-node drops the root npm cache (package-manager-cache: false); hosted
  jobs restore the pnpm store through the shared composite action, and
  qwen-autofix keeps af-012's rule that the persistent pool restores no
  remote cache.
- serve-ab and web-shell-visuals install a separate base checkout that can
  predate pnpm-lock.yaml, so those installs fall back to npm ci there.
- Out of scope, unchanged: packages with their own lockfile (cua-driver's
  TypeScript SDK, desktop-shell, live-host, mobile-mcp), and the triage
  lanes with npm-cache.yml and the npm audit gate, which move with the
  package-lock.json retirement.
- New size baselines for the touched workflows; qwen-autofix stays under
  the 470000-byte gate.

* fix(pnpm): link the workspace packages that root-level code imports

npm links every workspace package into the root node_modules, so
integration-tests/ and scripts/, which are not workspace packages
themselves, can import @qwen-code/* by name. pnpm links a workspace
package only where it is a declared dependency, and the root declared
none, so the no-AK integration gate could not find
@qwen-code/qwen-code-core.

The root now declares the seven packages that root-level code imports at
runtime as file: devDependencies: acp-bridge, channel-base, the CLI,
core, sdk, web-shell and web-templates. npm already links them, so its
tree does not change (letting npm re-resolve the lock reproduces the
same root entry); .pnpmfile.mjs rewrites them to workspace:*, which pnpm
links at the root.

* ci(pnpm): move triage, the store producer and the root audit to pnpm

- qwen-triage's verify and tmux-testing lanes install the PR with
  `corepack pnpm install --frozen-lockfile` from a restored pnpm store.
  The one retry clears node_modules first, because pnpm install, unlike
  npm ci, keeps it.
- npm-cache.yml becomes pnpm-store.yml, keyed on pnpm-lock.yaml.
- security-checks audits the root workspace with `pnpm audit`, which reads
  the lockfile, so the root install step goes. The vendored lockfiles keep
  npm, and the retry recognises pnpm's endpoint error too.
- qwen-autofix drops the hosted-only store cache: its heavy jobs forbid
  local `uses: './...'` actions, and the file sits at the size gate.
- cd-cua-driver: setup-node@v4 has no package-manager-cache input.
- package-assets fixtures install sharp rather than listing it in
  package-lock.json, matching how prepare-package now resolves it.
- AGENTS.md, CONTRIBUTING.md and the npm workspace guide install with pnpm.

* feat(review): install pnpm repos in qwen review's build/test

qwen review installed dependencies only for a repo with package-lock.json
and reported a pnpm repo as unsupported, so reviews of qwen-code would
lose their build and test once its npm lockfile goes.

- lockfileInstaller() picks npm or pnpm from the committed lockfile. With
  both, `packageManager: pnpm@...` chooses pnpm; anything else keeps npm.
- A pnpm repo installs with `corepack pnpm install --frozen-lockfile`
  through the same disk, budget and sandbox path as `npm ci`, and counts
  as installed once node_modules/.modules.yaml exists.
- Scripts still run through `npm run`, and the npm path is unchanged.

* ci(pnpm): retire the root package-lock.json

pnpm-lock.yaml is now the only root lockfile.

- check-lockfile keeps the pnpm integrity check and Playwright parity,
  which now reads pnpm importers and snapshots. The npm-vs-pnpm agreement
  and build-approval checks go: pnpm 11's strictDepBuilds and every CI
  install's --frozen-lockfile enforce what they covered.
- pnpm-lock-freshness.yml goes; it regenerated pnpm-lock.yaml from
  package-lock.json.
- Release: version.js drops its package-lock refresh, the release commit
  stages pnpm-lock.yaml and the SDK release stages no lockfile, so neither
  `git add` fails on the missing file. `npm version` in the SDK and
  mobile-mcp releases passes --no-workspaces-update so it cannot reinstall
  the root with npm.
- cd-mobile-mcp installs from the root pnpm lockfile. setup-node in
  cd-mobile-mcp, qwen-code-pr-review and desktop-release no longer keys an
  npm cache on the root lockfile.
- check-desktop-isolation reads pnpm importers; build.js, build_sandbox.js
  and preflight install with pnpm.
- pnpm-lock.yaml, pnpm-workspace.yaml and .pnpmfile.mjs join the autofix
  supply-chain class and repo-hygiene's deny list; pnpm-lock.yaml joins
  autofix's generated-diff exclusions, sdk-java's path filter and the
  platform-sensitivity manifests.
- .gitignore lists /package-lock.json so an npm install cannot commit it
  back. Tests and docs follow.

* fix(standalone): read the node-pty versions from the pnpm lockfile

* chore(vscode-ide-companion): regenerate NOTICES.txt after the lockfile swap

* chore(vscode-ide-companion): regenerate NOTICES.txt from the pnpm dependency tree

The committed NOTICES.txt was generated from an npm install and does not
match the tree produced by the pnpm install the release pipeline now uses
(659 resolved dependencies, differing @types/node and mime-db versions).
Regenerating with the pinned pnpm install makes the freshness check
reproducible.

* fix(ci): drop the npm cache from the daily dependency audit

actions/setup-node's npm cache keys off the root package-lock.json, which this branch retires, so the scheduled job would fail at Set up Node before the audit ran. No pull_request run reaches this workflow to catch it. The root tree is never installed here either: pnpm audit reads pnpm-lock.yaml directly.

Matches the sibling workflows that already set package-manager-cache: false; scripts/tests/security-workflows.test.js now pins it.

* chore(ci): stop banking main-side drift in the workflow size ratchet

The ratchet exists to turn a workflow's growth into one reviewed line. This branch re-recorded entries for 17 workflows it never touches, several upward by 0.4-2.8 KB - drift that happened on main, banked into the manifest without review.

Restore those to their merge-base values, so .size-baseline changes only for the 22 workflows this branch actually edits. The entries were not needed to go green: ci.yml passes the PR's base commit, so a file that drifted on main and is otherwise untouched earns a warning, not a failure.

* fix(release): initialize standalone state before main

* test(browser-use): read Playwright pin from pnpm lock

* fix(ci): address pnpm migration review blockers

* fix(desktop): stop reading the retired root lockfile

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-20 02:11:25 +00:00
Bob.qwencode
94946f1bf8
chore(release): v0.24.1 (#12239)
* chore(release): v0.24.1

* docs(changelog): sync for v0.24.1

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-19 08:23:48 +00:00
Jabrail
535dfd9a11
feat(web-shell): add PWA installability and Android development shell (#11722)
* feat(web-shell): add installable PWA support

* fix(build): skip incomplete Web Shell assets when bundling

* feat(web-shell): declare browser support matrix and compat fallbacks

* feat(web-shell): make the Web Shell installable as a PWA

Serves /manifest.webmanifest and /sw.js pre-auth, registers the worker
deferred to load, and replaces the earlier service-worker.js draft with
sw.js as a Vite entry (PNG icons included).

* feat(mobile-shell): add Android WebView shell skeleton

WebView shell around the daemon-served Web Shell (no second UI), profile
tuples of URL/token/display name with #token= fragment delivery, native
WebView version check at the Chrome 107 floor, and a foreground-service
skeleton for SSE/notifications.

* chore(web-shell): drop superseded service worker draft files

* test(web-shell): cover service worker routing and lifecycle

* chore(web-shell): align @playwright/test with main lockfile

* chore: register mobile-shell importer in pnpm lockfile

* test(web-shell): parse sidebar stylesheet with postcss for @supports nesting

* chore: register mobile-shell in pnpm rewrite set

* fix(web-shell): address PWA and Android acceptance blockers

* docs(mobile-shell): make remaining native acceptance limits explicit

* fix(web-shell): address fourth PWA review round

* fix(mobile-shell): restore web view on back

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-09-17 22:23:58 +00:00
Bob.qwencode
c53287da5f
chore(release): v0.24.0 (#12031)
* chore(release): v0.24.0

* docs(changelog): sync for v0.24.0

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-16 13:31:06 +00:00
qqqys
aff26a8f36
fix(cli): register model-invocable commands before headless turns (#11994)
* fix(cli): register model-invocable commands before headless turns

* test(ci): include headless workflow regression in expected no-AK suite

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
2026-09-16 07:35:21 +00:00
Bob.qwencode
af4da34719
chore(release): v0.23.4 (#11880)
* chore(release): v0.23.4

* docs(changelog): sync for v0.23.4

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-14 15:59:33 +00:00
易良
c666ec1a0a
fix(ci): restore Windows test baseline (#11787)
* fix(live): restore Windows filesystem compatibility

* fix(ci): restore Windows test baseline

* fix(ci): keep release-age excludes internal and re-arm two witnesses

- pnpm-workspace.yaml: drop '@tybys/wasm-util@0.10.4' from
  minimumReleaseAgeExclude. scripts/tests/package-scripts.test.js pins
  that list to internal, version-independent entries. The exclusion is
  also inert: minimumReleaseAge is not configured anywhere in the repo,
  and pnpm-lock.yaml records `overrides` but not minimumReleaseAgeExclude
  -- so the '@tybys/wasm-util': '0.10.4' override stays, because dropping
  it would desync the lockfile and fail --frozen-lockfile.
- web-shell-brand.test.ts: build the swapped-file fake as BigIntStats.
  Production compares bigint fields with a bare `!==`, so a number-backed
  fake tripped `dev` first and the perturbed inode was never reached;
  removing the perturbation now turns the test red again.
- run-qwen-serve.test.ts: '0' is unresolvable on Windows (getaddrinfo
  ENOTFOUND 0), so the wildcard case keeps '0.0.0.0' and witnesses the
  socket rather than a spelling: the operator typed port 0, so the report
  must carry the ephemeral port the listener actually got. Spelling
  normalisation stays pinned platform-independently in
  remote-quickstart.test.ts.
- artifact-snapshots.ts: revert the readdir hoist to main. It is
  behaviourally inert (the catch rethrows ENOENT on the operationId path
  either way), has no test and no Windows link.

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

* fix(ci): restore the load-bearing release-age exclude, keep the witness fixes

The previous commit dropped '@tybys/wasm-util@0.10.4' from
minimumReleaseAgeExclude to satisfy scripts/tests/package-scripts.test.js:390,
on the reasoning that the entry was inert because minimumReleaseAge is not
configured in this repo. That reasoning was wrong and CI measured it: the gate
is enforced by pnpm itself, not by repo config, so removing the exclude failed
'Install frozen pnpm worktree dependencies' on all three platforms with

  Lockfile failed supply-chain policy check (1975 entries)
  @tybys/wasm-util@0.10.4 was published at 2026-09-13T08:00:13.464Z,
  within the minimumReleaseAge cutoff (2026-09-12T20:03:50.286Z)

(run 34779568628; Install was green at 6099768ffb, which carries the entry).
Restoring the line makes pnpm-workspace.yaml byte-identical to that head again.

The three other changes from that commit are kept, since they are independent
of the exclude and were green in run 34779568625 (Test 18m58s, Lint & Static
12m36s, Integration no-AK 7m8s): the web-shell-brand.test.ts BigIntStats fake
that re-arms the inode witness, the run-qwen-serve.test.ts socket witness
replacing the tautological '0.0.0.0' assertion, and the artifact-snapshots.ts
revert to main.

This leaves the package-scripts.test.js assertion red exactly as it was before
this round. That is now a measured conflict rather than an open question: the
repo test pins the exclude list to internal, version-independent entries, while
pnpm needs a version-pinned exemption for a package published the same day.
Resolving it means either dropping the '@tybys/wasm-util': '0.10.4' override and
regenerating both lockfiles so an older, age-compliant version resolves, or
relaxing the invariant to admit external version-pinned excludes. Both are
supply-chain policy calls for the author/maintainer, so neither is made here.

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

* fix(ci): pin @tybys/wasm-util to the age-compliant 0.10.3 instead of exempting it

Test (ubuntu-latest, Node 22.x) failed at scripts/tests/package-scripts.test.js:390
(run 34781999892), which pins minimumReleaseAgeExclude to internal,
version-independent entries, while the previous head carried
'@tybys/wasm-util@0.10.4' there to satisfy pnpm's minimumReleaseAge gate. The two
looked mutually exclusive, but only because the pin chose the newest release.

@node-rs/jieba-wasm32-wasi pulls in @napi-rs/wasm-runtime@1.2.4, which accepts
^0.10.3, and 0.10.3 was published 2026-06-23 -- well outside the 24h window that
rejected 0.10.4 (published 2026-09-13T08:00:13Z). So the pnpm override now pins
0.10.3, the exclude entry is dropped, and both lockfiles follow: package-lock.json
must lock the same version or scripts/check-lockfile.js's "pnpm lockfile against
package-lock.json" agreement gate fails, which also reddens the two check-lockfile
fixture branches in package-scripts.test.js because they copy the real lockfiles.

The pin lives on the pnpm side only, mirroring how the typescript pins already
diverge; npm has no release-age gate, so no package.json override is added and the
nested @tailwindcss/oxide-wasm32-wasi copy of @tybys/wasm-util is left alone. Once
0.10.4 ages out of the window, a maintainer can drop the override from both sides if
nothing else needs the pin.

Verified locally: pnpm 11.24.0 `install --lockfile-only` reports "Lockfile passes
supply-chain policies (1975 entries)"; scripts/tests/package-scripts.test.js 49
passed / 1 skipped (was 3 failed); scripts/tests/check-lockfile.test.js 14 passed;
prettier --check clean.

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

* fix(ci): handle expected Windows socket reset

* fix(ci): make Windows regression tests portable

* fix(ci): make script tests portable on Windows

* test: re-arm four witnesses the review round found vacuous or over-gated

Each of these was measured, not argued; the mutation named in every case is
the one that used to leave the suite green.

- no-follow-open.test.ts: add the >2^53 identity case. Removing
  `{ bigint: true }` from all four production stat call sites left 17/17
  green, so the Windows-precision conversion this PR exists for was
  unwitnessed. The fixture uses 2^60 offsets, where the double spacing is
  256 and both ids collapse to one value; 2^53+1 / 2^53+2 do NOT collapse
  (2^53+2 is exactly representable), so those constants cannot pin it.
  Two-arm measured: intact 18/18 pass, mutated exactly 1 fails.
- web-shell-brand.test.ts: stat the planted regular file with
  `{ bigint: true }`. Production compares BigIntStats with a bare `!==`, so
  the number-backed fake tripped `dev` on a type mismatch alone and would
  have refused the correct file too. Probed both arms: pre-fix
  `typeof expectedStat.dev === 'number'` with the `dev` clause true,
  post-fix both sides bigint with `dev` false and the refusal driven by a
  real inode mismatch.
- conversation-workspace.test.ts: move the `inodeVerifiable` gate below the
  two `toBe(identity)` assertions. They are inode-independent, so gating
  them deleted a witness the Windows lane was green on. Modelled an
  unverifiable-inode host: the test reports skipped, not failed, which is
  only reachable once both assertions have passed.
- worktree.test.ts: split `localFilterCommands` in two. The fail-closed
  branch is platform-free by construction and the base Windows log shows it
  passing there; only the `a\nb` fixture is Windows-impossible, so only that
  half keeps the gate.

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

* ci: say why the post-redirect autocrlf line is not a duplicate

Both workflows now run `git config --global core.autocrlf false` twice: once
before the checkout and once inside the test step, where HOME/USERPROFILE are
redirected to a freshly created empty directory. The second line therefore
writes a different global config than its twin, and on ci.yml's hosted
windows-2022 fallback the pre-checkout step never runs at all because it is
gated on `runner.environment == 'self-hosted'`.

It was the only one of the three occurrences with no comment, so deduping it
looks free and silently returns CRLF conversion to every git operation the
suites perform. Replaying no-ak-integration-ci.test.js's own assertions with
the second occurrence deleted gives identical indices and still passes, so no
test notices the removal.

Comments only. The ordering assertions search for the FIRST occurrence and
none of the added text matches a needle they look for; the 14 assertions in
no-ak-integration-ci.test.js still pass.

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-14 11:17:19 +00:00
Tianyuan
7e0beb9d18
fix(skills): re-apply a Skill's side effects when a session is resumed (#11280)
* fix(skills): re-apply a Skill's side effects when a session is resumed

A resumed session restored a Skill's instructions without restoring the
state that enforces them: the restore path only rebuilt the dedup sets,
while session hooks and allowedTools allow rules live in per-process
memory. So --continue / --resume came back with the Skill's body in the
model's context and its PreToolUse gate gone, silently.

Each restored Skill now goes through the same shared side-effect helper
both live invocation paths use. Two refusals carry over from those paths,
so a resume never re-arms what a fresh invocation would refuse today: a
Skill disabled since it was invoked, and a conditional (paths:) Skill
whose activation has not fired in this session. Folder trust is re-checked
inside the helper, and both registrations dedup, so the replay cannot
stack duplicates. Bookkeeping stays unconditional, since the body really
is in the restored context.

The paths that still do not restore now say why in the debug log — at
warn when the Skill declares hooks:, matching the existing rule — instead
of falling through silently. A Skill started with /<skill-name> leaves no
tool-call record to key on and is documented as a limitation.

Fixes #11180

* fix(skills): scope a restored Skill's grants to its session and name what resume declines

Review round 1 on #11280.

A skill's `allowedTools` were granted as process-wide session allow rules
that no session transition clears, while its hooks die with the session
they were registered for. Re-applying them on resume made that gap
reachable from a failed `/resume`: the rollback returns the user to the
old session with the abandoned one's grants still live. Grants made for a
skill now carry the session they were made for, and the permission
manager filters on it alongside the existing folder-trust check, so
`/clear` and a session swap drop them the way they drop the skill body.
The dedup contract widens the same way trust gating does.

Restore also declined more cases than it explained, and explained one it
got wrong: a same-session re-invocation records the dedup message rather
than a body, and every such response was reported as an edited SKILL.md —
a steady-state warning pointing at a file that never changed. An already
restored skill is now a no-op, and the mismatch reason is chosen by
whether the recorded output is a skill body at all. A skill hidden from
the model since it was invoked is no longer re-armed: frontmatter is not
part of the recorded body, so that change is invisible to the match.

Also: collect the cached skills once instead of twice per resume, pin the
two `await`s and the mismatch log with tests that red without them, scope
the integration test's send wait to the request carrying its prompt, and
correct the skills doc — resume re-applies both halves, a frontmatter-only
edit is restored, and the cases that log elsewhere or not at all are named.

* fix(skills): single-source the skill availability rule resume re-checks

Review round 2 on #11280.

Resume decided whether to re-arm a restored skill's `allowedTools` and
`hooks:` from its own copy of the model-facing availability rule, which
the availability filter also hand-rolls. Two copies of one rule drift,
and this pair already had: the filter's visibility condition was missing
from the resume copy until last round. Both now ask
`skillModelInvocationBlock`, which returns which condition failed so the
resume path can still name it in the log — the reason that branch exists.
Every condition is read live off `Config` / `SkillManager` rather than
off the snapshots an unsequenced `refreshSkills()` commits, and
`pendingConditionalSkillNames` keeps its own filter, which is this trio
minus the activation check.

The session scoping added last round was pinned only against a stubbed
`PermissionManagerConfig`, leaving the wiring it depends on — that the id
tagged onto the grant is the one `startNewSession` replaces — untested.
A `config.test.ts` case now drives a real `Config`, a real
`PermissionManager` and `applySkillSideEffects` across a session
boundary, and reds if either the scoping or the id passed through
`applySkillSideEffects` is removed.

Both `PreToolUse`-gate integration suites drove copies of one fixture,
already diverged in the place that decides their assertions: only the
resume suite's gate wrote the hit counter, and nothing at the parity call
site said why. The skill, the gate script (counter always written), the
fake-model env stubs, the launch args, the poll helper and the exit
sequence move to `integration-tests/helpers/gated-skill-fixture.ts`. The
two fake models stay separate: parity dispatches on a request counter,
resume on the last user message, because its second session replays the
first one's turns.

* fix(skills): scope the AUTO stash, key resume declines on the record

Round 2 review follow-ups.

R1-1 (Critical): `addSessionAllowRule`'s AUTO-mode stash branch deduped on
`raw` and returned before the live re-point, so a dangerous `allowedTools`
grant stashed in session A kept A's id; leaving AUTO re-attached that same
object and the session filter dropped it for good. It now updates the stashed
entry in place, never pushing it into `sessionRules.allow` while AUTO is on.

R2-3: `activeSessionAllowRules()` was the only reader applying the session
predicate. `getAllowRawStrings()` (the `StartSessionEvent` at a session
boundary) now goes through it, and the AUTO strip candidates go through the
new session-only half — not the full predicate, so a mid-AUTO trust grant
cannot activate an unstripped dangerous rule.

R1-3: `skillModelInvocationBlock` checks `disableModelInvocation` before
`isSkillActive`. Such a skill never enters the activation registry, so
`inactive` is permanently unsatisfiable and masked the cause that explains
the decline.

R2-4: the decline log is deferred to after the history scan and emitted only
for skills nothing later restored, so a stale first pair no longer warns
"your gate is gone" about a gate the same loop arms.

R2-7: `logSkillNotRestored` keys on the record rather than the declaration.
A non-body record (a recorded refusal) logs at debug saying nothing was ever
armed; the warn level now covers `allowedTools` as well as `hooks:` (nine
bundled skills declare the former and none the latter); and each reason names
the route that works — `/<skill-name>` for inactive and hidden, `/skills`
first for disabled.

R1-5, R2-1, R2-6: the docs bullet, the `sessionId` JSDoc and the
`SkillCommandLoader` assertion comment now describe what the code does — a
recorded-response mismatch covers a moved skill directory as well as an
edited body, no in-tree caller produces an unscoped session grant, and the
"Exactly `{ trustGated: true }`" pin covers both keys.

R2-2, R2-8: the shared fixture derives `SKILL_DESCRIPTION` from its prefix,
names each generated on-disk path once, is imported by the parity suite
instead of re-spelling `gated-skill`, and emits the description as a quoted
YAML scalar — a bare one with a colon-space flattens the `hooks:` block into
`{"PreToolUse":"","- matcher":"Shell"}`, which `parseHooksConfig` discards,
leaving the gate silently absent.

* fix(skills): hold the AUTO and trust invariants at the read side on resume

Round-3 review findings on #11280, fixed at the shared cause rather than
per instance.

`sessionRules.allow` keeps a previous session's grants as inert entries and
nothing ever removes one, so every path that reads the array had to
re-derive "is this rule mine?" — and each derived it differently:

- The AUTO strip takes candidates from the session-scoped half, so a grant
  tagged with another session was neither stripped nor stashed, and the
  strip's latch never re-scans. Re-entering that session id under a latched
  AUTO made the rule live again with the classifier bypassed. Enforce the
  AUTO half in `activeSessionAllowRules()`, the predicate `evaluate()`
  decides on, where session provenance cannot defeat it.
- `restoreDangerousRules()` re-attached the stash by concatenation, seating
  two entries with the same `raw` once a cycle spanned a session swap. Merge
  by `raw`, keeping the stashed object so the next strip can still remove it
  by identity.
- The live dedup re-pointed a kept entry across a session boundary while
  keeping the dead session's `trustGated`, so a previous session's ungated
  grant de-gated a later project skill's repository-controlled one and the
  folder-trust gate failed open. Take the arrival's gating on a cross-scope
  re-point, widen only within one scope, and route the AUTO stash branch
  through the same rule instead of re-pointing the session alone.

On the resume path:

- A skill body the scheduler truncated before recording starts with the
  truncation sentinel, not the body prefix, so it was classified as "not
  this skill's body" and declined at `debug` under a sentence asserting
  nothing was ever armed — false for a record whose `execute()` had already
  applied the side effects. Classify it as the truncated body it is. The
  decline stands: the record can no longer be compared against SKILL.md.
- Declines were keyed last-write-wins, so a later refusal pair overwrote a
  body-shaped decline and discarded the one actionable signal. Rank them.
- The body-mismatch arm hardcoded `reinvoke`, which `validateToolParams`
  refuses whenever a block condition co-occurs — and it does by
  construction for a `paths:` skill. Consult the block for the remedy.
- Remedies named `/<skill-name>` unconditionally, but `user-invocable:
  false` drops the command from every dispatch accessor. Branch on it.

Also corrects the in-code rationale for the warn/debug split: `warn` and
`debug` share one sink gated only on `QWEN_DEBUG_LOG_FILE`, so the level
buys grep and triage inside the debug log, not operator visibility.

Every behavioural fix is pinned by a test verified to red when the fix is
removed.

* fix(skills): read unmatched resume records as uncorroborated bodies

Addresses the round-4 review of the resume re-arming path.

- An unmatched recorded response is now reported as a declined body
  (warn, with a route) unless it is one of the strings execute() returns
  in place of a body. The persisted-output stub, the budget-exhausted
  stub and the spill-failure fallback no longer land in the arm that
  claims nothing was armed; none of them is re-armed.
- The restore lookup keys on the exact skill name, as the live path
  does, so two skills whose names differ only by case no longer collapse
  into one entry.
- A declined project skill in an untrusted folder names folder trust as
  the precondition, and a disabled user-invocable: false skill is no
  longer sent to /skills.
- Pin the stash path's cross-session trust narrowing and the merge's
  object replacement in permission-manager tests, and correct the merge
  comment's stated reason.

* refactor(skills): simplify resume re-arming

- Drop session-scoped allow rules. Config.startNewSession now clears
  session allow rules (including the AUTO stash) when the session
  changes, and a resumed session re-arms its own skills during
  initialize(). This removes the sessionId tag and its AUTO-mode merge
  and trust re-pointing logic.
- Replace the decline classification, remedy text and warn/debug ranking
  with one factual warn line for a skipped skill that declares hooks or
  allowedTools.
- Trim comments and the docs bullet.

* fix(skills): do not re-arm Skills recorded inside exec output

An exec script can print the nested-results line itself, so it now restores
the Skill body only. The docs now state which skips log and that the match
covers the body only.

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-09-14 01:02:42 +00:00
jinye
ef0b8a6baf
fix(ci): raise the build heap cap from 3072 to 4096 MB (#11781)
main alone peaks at ~3.14 GB RSS building packages/cli under the 3072 MB
--max-old-space-size cap, so whether tsc --build survives is decided by
runner load: under CPU contention GC throughput drops and the build dies
with 'Ineffective mark-compacts near heap limit'. Three OOM failures on
three distinct runners versus green builds of the identical commit on
quieter ones were observed on a single PR head.

4096 gives ~30% headroom over the measured footprint; worst-case RSS
(~4.3 GB) stays safe on GitHub-hosted 16 GB runners and the ECS fleet.
The test lanes keep 3072 — per-package vitest heaps are far smaller and
have no OOM evidence.

Fixes #11780
2026-09-13 21:46:19 +00:00
Dragon
4ec9f63d0a
feat(core): add CodeModeOnly tool execution (#10607)
* feat(core): add CodeModeOnly tool execution

* fix(core): preserve CodeModeOnly execution semantics

* fix(core): drop the tool_search reminder in CodeModeOnly

CodeModeOnly hides `tool_search` and binds every deferred tool — schema
included — into the `exec` description, so progressive discovery has
nothing left to do. The session prelude still announced those tools as
"reachable via `tool_search`", pointing the model at a tool that is
never declared and re-billing the text on every cached prefix. Worse,
tools already callable as `tools.web_fetch(...)` looked gated behind a
lookup step that does not exist.

The subagent and fork-resume callers already opted out, but the
main-session prelude sites did not, so the gate now lives where the
prelude is built rather than in each caller.

* chore: refresh settings schema

* chore: refresh companion notices

* fix: register exec tool metadata

* fix(cli): localize exec tool display name

* fix(core): stop offering ToolSearch lookups in CodeModeOnly

CodeModeOnly hides tool_search and binds every deferred schema into the
exec description, so text that tells the model to look a tool up
describes a surface it cannot reach. Return an empty deferred summary
from the registry itself - this also covers the fork-resume reminder
that bypassed the earlier call-site gate - and point the image zoom
hint at tools.zoom_image.

* feat(core): teach the system prompt CodeModeOnly's tool surface

In CodeModeOnly the model can only call exec, so the tool guidance and the
worked examples described a surface it does not have: bare tool names it
cannot call, an instruction to issue several tool calls in one response, and
examples in a direct tool-call syntax that does not exist there.

Give "Using Your Tools" a code-mode branch that routes every tool through
tools.<name>, states which tools are direct controls instead, and replaces
multi-call parallelism with batching inside one exec program. Swap the four
model-family example sets for one shared exec set, since the syntax
differences they exist for do not apply. The mechanics of exec itself stay in
its tool description; the prompt carries only policy.

* test: include code mode host in standalone fixture

* fix(core): return code mode media as multimodal output

* feat(core): support image tool results in code mode

* fix(core): carry deferred tool schemas in the exec description

CodeModeOnly hides tool_search and never surfaces a nested call as a history
functionCall, so a signature collapsed to Record<string, unknown> could never
be filled in later: parameter names were reachable only by guessing, or by
reading them off validation errors one at a time. Deferred tools keep their
registry state; only the generated signature gains the schema.

* fix(node-repl): restore the bin exec bit after rebuild

npm chmods dist/index.js when it links the bin at install time, but the
build deletes that exact file and tsc re-emits it as 0644. After any
rebuild node_modules/.bin/node-repl-mcp therefore points at a
non-executable target and spawning it fails with EACCES (exit 126)
before the shebang is ever read.

OR the exec bits into the emitted mode instead of setting 0o755, so a
restrictive umask is preserved rather than widened.

* fix(cli): pin patched Ink version

* feat(cli): add collapsible tool call details

* fix(dev): use CLI tsconfig outside repository

* fix(cli): preserve expanded tool details after completion

* fix(cli): fail headless runs on model API errors

* fix(core): expose shell timeout bounds in code mode

* feat(core): add code mode timeout helpers

* feat(cli): expose code mode in settings

* fix(mcp): preserve structured results for model and exec consumers

* fix(core): preserve code mode output and truncate inline

* feat(core): port scoped context tools to code mode

* fix(build): synchronize pnpm lockfile and capability docs

* fix(core): recover from rejected Responses encrypted reasoning
2026-09-11 07:40:35 +00:00
Bob.qwencode
38513b591b
chore(release): v0.23.3 (#11583)
* chore(release): v0.23.3

* docs(changelog): sync for v0.23.3

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-10 15:03:19 +00:00
Bob.qwencode
bae90d7afa
chore(release): v0.23.2 (#11469)
* chore(release): v0.23.2

* docs(changelog): sync for v0.23.2

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-09 11:38:23 +00:00
易良
b661a2cdb2
fix(deps): Bump sharp to 0.35.4 to resolve high-severity libheif CVE (#11435)
* fix(deps): Bump sharp to 0.35.4 to resolve high-severity libheif CVE

The dependency CVE audit gates on high-severity findings. sharp
0.35.3 pulls in a vulnerable libheif (GHSA-rgj7-g3m4-5g8c), which
fails the audit. Bump sharp (and its @img platform/libvips binaries)
to 0.35.4, which ships the fixed libvips 1.3.3.

* fix(deps): regenerate pnpm-lock.yaml for sharp 0.35.4

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

* chore(deps): Refresh VS Code companion notices for sharp 0.35.4

The sharp bump changes the generated NOTICES.txt version string;
Lint & Static gates on the file being up-to-date.

* chore(deps): Clear transitive sharp 0.35.3 from pnpm lockfile

@mobilewright/core (mobile-mcp) still resolved sharp 0.35.3 through
its ^0.35.3 range, leaving the vulnerable libheif build reachable
from the worktree bootstrap. Refresh the resolution so every sharp
and @img platform entry in the pnpm lockfile is 0.35.4 / 1.3.3.

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-09 06:37:26 +00:00
Bob.qwencode
c26f6d299f
chore(release): v0.23.1 (#11400)
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
pnpm Worktree Smoke / Install (macos-latest) (push) Waiting to run
pnpm Worktree Smoke / Install (ubuntu-latest) (push) Waiting to run
pnpm Worktree Smoke / Install (windows-latest) (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
* chore(release): v0.23.1

* docs(changelog): sync for v0.23.1

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-08 17:01:17 +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
易良
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
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
易良
5b470befa0
ci: make the Prettier lane a real gate (#11117)
* chore: format the 35 files CI had been silently rewriting

`scripts/lint.js --prettier` and the release workflow's `Format Project`
step both run `prettier --write`, which reformats in place and exits 0.
Neither is followed by a dirty-tree check, so the rewrites are discarded
when the job ends and these files have stayed unformatted on `main`.

This is the output of `prettier --write` over exactly those files, so the
next commit can turn the check into a real gate without failing on a
backlog. No behaviour change: every hunk is whitespace, quoting or
wrapping.

Verified with prettier 3.6.1, the version pinned in package-lock.json.
`prettier --check .` over the whole repo reports these 35 and nothing
else.

Refs #11109

* fix(ci): make the Prettier lane a real gate

`runPrettier()` ran `prettier --write .`. Prettier exits 0 after
reformatting, and no caller checks the tree afterwards, so the lane
passed on unformatted code and CI threw the rewrites away — the 35 files
in the previous commit are what it had been silently fixing and
discarding.

ESLint does not cover the gap: eslint.config.js loads
eslint-config-prettier, which disables formatting rules rather than
enforcing them. With this change the only enforcement point outside the
husky pre-commit hook actually enforces something.

`npm run lint:all` now reports unformatted files instead of fixing them;
`npm run format` is the fix. Prettier's own failure message says so.

Refs #11109

* fix(release): drop Format Project, it writes and discards

The step ran `npm run format` (`prettier --write .`) and nothing in
release.yml inspected the tree afterwards — no `git diff --exit-code`, no
`git status --porcelain` anywhere in the file. The rewrites died with the
job, so the only failure it could produce was Prettier crashing on an
unparsable file, which the Run Lint step reports anyway.

It cost 13m58s on a quiet host and 20m45s on a contended one, against a
30-minute cap that `quality_static` hit twice today (runs 33957952281 and
33963757913, both cancelled mid-lint with nothing failing). The remaining
steps are `npm ci` (1m44s) and `lint:ci` (~8m20s), so the job now has real
headroom instead of finishing 24 seconds over the wall.

Formatting is now gated where it belongs: the previous commit makes the
PR-CI Prettier lane `--check`.

Refs #11109

* fix(ci): keep prettier --check out of .qwen/ and the pre-paint theme script

Two things the `--write` → `--check` flip broke that `--write` had hidden.

`.qwen/` is gitignored as `.qwen/*` and holds machine-written state —
pending-skills/, auto-skill-*/, learned-skill-*/,
computer-use/installed.json. Prettier does not read .gitignore, so it was
walking in and silently reformatting whatever a contributor had there;
under `--check` that turns into a failed `npm run lint:all` on files git
does not even track. CI never saw it (fresh checkout, tracked files only),
which is exactly why it went unnoticed. Adding `.qwen/` to
.prettierignore also drops the one tracked file under it —
skills/find-simplifications/SKILL.md, force-added past .gitignore — from
the previous formatting commit, since a directory the gate should not
police should not be reformatted for it either.

The inline theme script in web-shell's index.html is restored to its
original single line behind `<!-- prettier-ignore -->`. It is a
render-blocking script in <head> that applies the stored theme before
first paint; the expanded form ships ~600 more bytes on the critical path
and buys no readability the comment above it does not already give. A
change about CI hygiene should not be reshaping that.

Refs #11109

* fix(ci): keep prettier --check out of vendored packages/mobile-mcp

eslint.config.js has ignored `packages/mobile-mcp/**` since it landed —
"vendored mobile-next/mobile-mcp; has own eslint config" — and
.prettierignore already carries its sibling `packages/cua-driver/`, listed
one line above with the same rationale. mobile-mcp was simply missed.

`--write` hid that: it reformatted the vendored package's own eslint
config to our house style every run and threw the result away. The
formatting commit in this PR would have made that permanent, widening the
diff against upstream on the next sync for a package this repo has already
decided not to lint. That file is reverted here and the directory is
ignored.

Same class as the .qwen/ fix in the previous commit: the gate reaching
somewhere the repo had already decided it should not.

Refs #11109

* fix(ci): mirror eslint's remaining global ignores in .prettierignore

`--check` makes .prettierignore load-bearing in a way `--write` never was:
under `--write` a file prettier should not touch was reformatted and the
result discarded, so nobody noticed; under `--check` it fails the lane.

These three are the rest of eslint.config.js's global ignores, added for
the reasons it already states. `scripts/codemod/fixtures/` is the one that
matters: those files are "intentionally non-idiomatic ink input/output",
so the obvious response to a `--check` failure there — reformat to house
style — would corrupt the fixtures the codemod tests assert against. The
trap is not live today (all three currently satisfy prettier), which is
precisely why it is worth closing before someone meets it.

Refs #11109

* chore: format converter.test.ts, which drifted onto main since this branch

This branch merged main 32 commits after its base and `prettier --check .`
found one new offender. It is the same gate failing the same way: nothing
enforces formatting, so `--write` reformatted this file on every CI run
since it landed and threw the result away.

Worth noting as evidence rather than noise — the backlog this PR fixes was
not a one-off from before someone got careless; it accrues continuously,
and it accrued again while the PR was open.

Refs #11109

* Revert "fix(release): drop Format Project, it writes and discards"

This reverts the release.yml half of this PR. #11195 removes that step on
its own, in one file and four lines, and two PRs racing on the same
deletion helps nobody.

The reasoning and the evidence are unchanged and now live on #11195: the
dry-run release dispatched from this branch with the step removed
(34022919438) went 18/18 green in 65m43s, quality_static finished in
7m00s against main's 8m57s, and Run Lint was 4m52s vs 4m43s — nothing
downstream ever read what the step produced.

Dropping release.yml from this PR also widens who can review it. It was
the only file here owned by @pomelo-nwu and @wenshao alone; what remains
under CODEOWNERS is /packages/core/, whose owner list is six people. The
Prettier gate no longer waits on the two maintainers who must sign off on
release workflow changes.

Refs #11109, #11195

* fix(ci): make the gate resolve the file set npm run format resolves

Two review findings, one root cause: the gate was checking a different set
of files than the tree it is supposed to police.

**The `.qwen/` ignore rested on a false premise (R1-1, @chiga0).** Prettier
3.6's `--ignore-path` already defaults to `[.gitignore, .prettierignore]`,
so every machine-written path that entry's comment named —
`pending-skills/`, `auto-skill-*/`, `learned-skill-*/` — was outside the
gate before it via `.gitignore:32`. What the entry actually excluded is the
corpus `.gitignore` deliberately re-includes: `.qwen/commands/**`,
`skills/**`, `agents/**`, `team-memory/**`, `review-context.json` — 57
tracked, hand-authored files. The gate then passed because the surface had
been narrowed, not because the tree was formatted, and contributors editing
those docs could ship them unformatted with both enforcement points silent.
The entry is dropped and the first commit's formatting of
`skills/find-simplifications/SKILL.md`, reverted alongside it, is re-landed.

**The gate and the remedy used different CLIs (R1-2).** `runPrettier()` ran
the classic `prettier --check .` while `npm run format` runs
`prettier --experimental-cli --write .`. The classic CLI reads only the
repo-root `.gitignore`; the experimental one also honours nested ones, and
this repo has 23 of those. Verified on a scratch tree: given
`pkg/.gitignore` containing `lib/`, classic reports `pkg/lib/nested.js` and
experimental skips it. So the lane could go red on a locally built package's
output that `npm run format` provably cannot clear. The gate now uses the
same CLI as the remedy it names.

Refs #11109

* fix(ci): let the pre-commit hook cover what the gate now checks, and pin it

**The hook was narrower than the gate (R1-5).** `lint-staged` formatted only
`*.{js,jsx,ts,tsx}` and `*.{json,md}`, but the gate checks every extension
Prettier has a parser for. Of the 35 files the first commit formatted, 29
(83%) are in classes the hook never touched — 13 `.yml`, 12 `.mjs`, 3
`.css`, 1 `.html`. Left alone, the backlog this PR just cleared would
re-accumulate, and now as a red CI step on someone else's PR rather than as
silent drift. `.mjs`, `.cjs`, `.yml`, `.yaml`, `.css` and `.html` join the
Prettier-only group; ESLint's globs are unchanged, since the gap is about
formatting coverage, not lint coverage.

**Nothing pinned the flag (R1-4).** `scripts/tests/lint.test.js` exercised
only the linter-directory helpers, and `ci-platform-lanes.test.js` asserts
that the `Run Prettier` step exists and is profile-gated, not what it runs.
A refactor, or a well-meant "make lint auto-fix again", could restore the
silent pass with every suite green. Two assertions now pin it: that
`runPrettier()` checks rather than writes, and that it uses the same
Prettier CLI as `npm run format`, so the two cannot drift apart again the
way R1-2 found them.

Refs #11109

* docs: correct the instructions and comments the --check flip falsified

**Three tracked instructions to automation agents now say the opposite of
what the code does (R1-9), and one is fed to a CI lane today** —
`.github/workflows/qwen-triage.yml` re-pins `verify-pr` from the base commit
and hands it to the verifying agent.

- `verify-pr/SKILL.md` told agents never to run `scripts/lint.js` with no
  arguments because it would rewrite the working tree under their A/B
  harness. That is no longer why. The prohibition is kept — it is still
  right — but re-grounded in what the no-arg form actually does now
  (`setupLinters()` wipes the linter temp dir and re-downloads three pinned
  binaries), and `--prettier` joins the enumerated non-mutating checks it
  was the only flag missing from.
- `find-simplifications/SKILL.md` and its `references/land.md` told the
  simplification agent that a formatting diff "carries zero signal" because
  CI runs `--write`. An agent believing that would skip `npm run format`
  after a deletion changed line shape — on the very extensions the hook does
  not cover — and go red on formatting it was told could not matter.
- `update-ecs-runner-qwen-workflow.test.js`'s comment justified parsing two
  matrix layouts by "CI runs `prettier --write .` before this suite". CI no
  longer rewrites anything; the two layouts are still worth parsing, so the
  comment is re-grounded in the checked-in form versus older branches.

**The index.html rationale overstated its own number (R1-8, @chiga0).** It
claimed the expanded script ships "~600 more bytes"; measured against this
repo's Prettier 3.6.1 and its `tabWidth: 2`, it is +258 source and +8
gzipped here, +287/+20 by the reviewer's method — either way roughly half
what was claimed, and "~600" is what the same expansion costs at
`--tab-width 4`. Since the prose ships verbatim inside the built
`index.html`, the fix is to drop the contested figure and keep the comment
short. The `<!-- prettier-ignore -->` guard itself stays: it is load-bearing.

Refs #11109

* chore: format classify-profile.test.mjs, drifted again after commit 1

Third instance in this PR's lifetime, and the sharpest: commit `b1b94a8762`
formatted this exact file as one of the 35, and `main` reintroduced
unformatted content in it within a day. The other two were
`converter.test.ts` at the first merge and this file at the second.

The gate is the fix; until it lands, every merge from `main` re-opens the
same hole.

Refs #11109
2026-09-07 00:44:01 +00:00
Tianyuan
92a8a8d179
fix(skills): register frontmatter hooks on the /<skill-name> path (#11068)
* fix(skills): register frontmatter hooks on the /<skill-name> path

A skill's frontmatter `hooks:` were registered only by `SkillTool`, the
model-invocation path. The user-invocation path — `SkillCommandLoader` and
`BundledSkillLoader`, reached by typing `/<skill-name>` — injected the skill
body and granted its `allowedTools`, but never registered its hooks.

A `PreToolUse` gate is the one mechanism that enforces a skill's rule
regardless of what the model decides to do, so users reach for it exactly
where a model mistake is expensive. Dropping it on the slash-command path
made that gate fail open, silently: nothing in the UI or the logs indicated
that a declared hook had been skipped.

Extract the side-effect application into `applySkillSideEffects` in
`skill-utils.ts`, alongside the existing `applySkillAllowedTools` and
`canApplySkillSideEffects`, and route all three call sites through it. Both
underlying registrations already dedup, and project skills stay trust-gated
on every path. Bundling the two side effects behind one helper also stops a
future invocation path from applying half of them.

Documents the `hooks:` frontmatter in the Skills guide, which had no user
documentation for it, making the deterministic path hard to discover.

Tests: unit regressions on both loaders (the two positive cases fail without
this fix), plus an end-to-end parity test driving the real interactive CLI
over a PTY, asserting the gate blocks the shell call on both paths.

Fixes #11067

* fix(skills): address review — sandbox reachability, CI coverage, resume path

R1-1: the parity test bound the fake server to loopback and hand-stubbed
NO_PROXY, so under the docker/podman sandbox legs the containerized CLI could
not reach it and both cases failed. Use `fakeServerHostOptions()` and
`applyContainerSandboxNoProxy()` like every sibling fake-server test; both are
no-ops outside a container sandbox.

R1-2: the test sat outside every suite that gates a PR, so a regression would
only surface post-merge. Add it to `test:integration:no-ak:sandbox:none` (it
needs no credentials) and to the pinned list in the CI wiring guard test.

R1-4: session hooks are in-memory only, so `--continue` / `--resume` restored a
skill's body without its side effects — and the dedup guard answers "already
loaded in context", so nothing prompted the re-invocation that would have
re-registered them. The gate silently stopped firing after a resume: the same
fail-open shape this PR fixes. `restoreLoadedSkillsFromHistory` now re-applies
side effects per restored skill, through `applySkillSideEffects` so the
folder-trust gate still applies. Documentation says so.

R1-5: restore the trace the refactor dropped. Sessions that disable hooks
(`disableAllHooks`, safe mode, bare mode, ACP `skipHooks`) build no hook
system, so a skill's gate is discarded — previously with a debug line naming
`hasHookSystem: false`, now silently.

R1-6: cover the guard branch. It is reachable in hooks-disabled sessions, and
deleting it would throw on `undefined` and crash every skill invocation there,
with nothing going red.

R1-3: `[hook](hooks)` 404s on GitHub — the target is `hooks.md`, and no
docs-site generator resolves extensionless links.

Each fix was verified to red when the fix is removed.

* fix(skills): don't re-arm disabled skills on resume; correct the docs claim

Both problems were introduced by the resume repair added in the previous
round, and both have the shape this PR exists to close: an operator believes
a gate is armed when it is not.

R2-1: `restoreLoadedSkillsFromHistory` applied side effects for every matched
skill with no enabledness check. `applySkillSideEffects` gates only folder
trust, and the candidate map comes from `getCachedSkills()`, which does not
consult `skills.disabled` — so invoke-skill → disable → `--continue`
reinstated the skill's `allowedTools` auto-approval rules and re-registered
its hooks. Both live paths refuse a disabled skill before applying anything
(`executeDisabledSkill`; the loader's disabled branch), so restore was the
only way back in. Gate the side-effect call on `isSkillEnabled`; bookkeeping
of `loadedSkillNames`/`loadedSkillContents` stays unconditional, since the
body is in the restored context either way and the dedup guard must know.

R1-5: the docs bullet claimed resume re-registers hooks on both invocation
paths. It does not. The restore matcher requires a `ToolNames.SKILL`
functionCall/functionResponse pair, which only the model path writes; the
slash command submits the body as an ordinary prompt and leaves no such
record, so a `/<skill-name>` skill's hooks are gone after resume while its
instructions stay resident. State the truth per path, say how to re-arm the
gate, and note the disabled-skill case. Making the parity claim true needs a
restorable trace for the slash path — out of scope here.

Tests: a probe asserting zero hook and zero allow-rule calls for a disabled
skill on restore, verified to red when the enabledness gate is removed.

* fix(skills): drop the resume repair, returning this PR to its original scope

The resume repair added in round 1 has produced one Critical and three
Suggestions across two review rounds, all in the same two files, and none of
them are the bug this PR set out to fix. Both maintainers and the review bot
converged on the same advice: the cluster belongs in its own change.

Removing it entirely, rather than continuing to patch it, is also the honest
call. The integration leg written for R3-3 showed the repair does not work in
a real resumed session: the gate fired 0 times after `--continue` and the
shell command actually ran, while the unit tests stayed green because they
mock the hook system wholesale. Shipping a re-arm that does not re-arm would
reproduce the exact fail-open this PR exists to close.

`restoreLoadedSkillsFromHistory` is now byte-identical to its pre-PR form.
The docs bullet takes round 1's other offered option and states the truth for
the reverted behavior: session hooks are in-memory only and are not restored
on resume, on either path — re-run the Skill to re-arm its gate.

What remains is the change reviewed as sound: one shared side-effect helper,
both loader paths routed through it, and the tests and documentation for it.
The resume gap is pre-existing, now documented, and left to a follow-up.

Removed: the resume re-arm and its `isSkillEnabled` / `pendingConditionalSkillNames`
gates, the mismatch-branch trace, three resume unit tests, and the resume
integration leg.

* test(skills): close the three suggestions left open from rounds 1-2

R1-6 (fix-induced): the trace added for the silent-drop finding was `debug`,
which `debugLogger` drops entirely without `QWEN_DEBUG_LOG_FILE` — quiet was
asked for, invisible was not. Raise it to `warn`: control only reaches this
branch for a skill that actually declares hooks (`applySkillHooks` returns
early on `!skill.hooks`), so it fires exactly when a promised gate is being
dropped and cannot become a steady-state warning. The docs sold the
guarantee without naming the exception, so the Notes list now says hooks are
not registered in sessions that disable them (`disableAllHooks`, safe mode,
bare mode, an ACP client's `skipHooks`), where the body and `allowedTools`
still apply but the gate does not.

R2-3: the no-session-id test pinned only the hooks half, so hoisting that
guard above `applySkillAllowedTools` shipped untested. It now asserts the
grant still lands, matching its no-hook-system sibling.

R2-2: the parity test sequenced its legs with unconditional `setTimeout`
sleeps that neither shortened on a fast runner nor extended on a slow one.
Every wait now polls the condition it is actually waiting on — the command
echoing, the model call the submitted body produces, the gate firing, the
exit prompt. Incidentally 2.4x faster locally (15.6s -> 6.6s), which is the
fixed budget that was being burned.

Both new assertions are load-bearing: reverting the trace to `debug` reds the
no-hook-system case, and hoisting the session-id guard above the allowedTools
grant reds the no-session-id case.

* fix(skills): correct the trust claim, pin the hookless early return, fix the recipe

All four from @wenshao's round-3 verification report.

The Notes list claimed folder trust is re-checked at fire time, so revoking
it mid-session silences a project Skill's hooks without a restart. Measured
on the real CLI, neither direction takes effect: `Config.trustedFolder` is
`private readonly`, assigned once in the constructor with no setter, so the
fire-time check returns a constant for the life of a Config — and both of the
CLI's own trust-change paths set `needsRestart`. The sentence was copied
faithfully from a pre-existing code comment, but this PR is what promotes it
into user-facing docs where a reader will act on it, which is exactly the
shape of overpromise this PR exists to remove. It now says trust changes take
effect on restart.

The surviving mutant: deleting `if (!skill.hooks) return;` from
`applySkillHooks` left the whole core suite green, while that early return is
what lets the branch below it be a `warn` rather than steady-state noise —
without it every hookless Skill invoked in a hooks-disabled session warns.
Pinned by a case that reds when it is removed.

Two fixes to the doc recipe: it is the first copy-pasteable hook example, and
run verbatim with the script left at 0644 the gate fails open silently, so it
now says to `chmod +x`. The example also used `matcher: Shell`, which works
but is the compatibility alias; hooks.md tells new configs to prefer runtime
ids, so it uses `run_shell_command`.

Also fixes a soundness bug the new assertion exposed: `debugLoggerSpies` is
module-scoped and was never cleared between cases, so log assertions were
reading calls made by earlier tests. Cleared in `beforeEach`, which makes the
previous round's two log assertions meaningful as well.

* fix(skills): correct the trust claim, guard an empty hooks block, wait for command registration

Round 4 review (#11068): two Criticals and two Suggestions, all four verified
locally with intact/flip arms rather than by reading the diff.

- docs: the trust bullet rewritten in 80c2fcb asserted unconditionally that a
  mid-session trust change takes effect only on restart. `isTrustedFolder()`
  reads the IDE context store first and falls back to the readonly
  `trustedFolder` field only when the store is empty, so the value is live
  under an IDE connection. A probe on a real Config confirms it: with
  `trustedFolder: true`, an `ideContextStore.set({workspaceState:
  {isTrusted:false}})` flips `isTrustedFolder()` to false mid-session, and
  clearing the store restores true. State both branches instead, and align the
  `applySkillHooks` / `applySkillAllowedTools` JSDoc with the same wording so
  the two documents stop disagreeing about one gate.

- docs: bare mode was listed with the sessions where only hooks are dropped.
  `listSkillsAtLevel` returns [] for every level in bare mode and the tool
  registry returns before the Skill tool is registered, so there is no body and
  no allowedTools there either. Safe mode differs — discovery is limited to
  ['bundled'] rather than emptied — so it stays in the first group.

- `applySkillHooks`: `{}` is truthy, and `parseSkillContent` assigns an empty
  object for `hooks: {}` and for a block whose event names are all unknown. Such
  a skill fell through the `!skill.hooks` guard and, with the warn level this
  branch now uses, emitted a per-invocation warning about dropping a gate it
  never declared. Widen the guard and pin the empty-object shape with a test;
  dropping the new operand turns that test red and nothing else.

- parity test: the poll that replaced the fixed sleep waited for the typed text
  to echo, which lands before the skill command registry does. Submitting into
  that gap gets `Unknown command: /gated-skill`, undetectable downstream because
  the error text contains the command. Poll for the completion menu rendering
  the skill's own description — a signal only a registered command produces —
  and assert the unknown-command error never appears.

Verified with the skill command loader artificially slowed by 1500ms, same
bundle, only the wait predicate differing: the echo poll fails 3/3 on the
user-invoked leg with `Unknown command: /gated-skill`, the registration poll
passes 3/3 and absorbs the delay (4.3s vs 3.0s). Unslowed, the fix arm is 3/3
green with zero unknown-command occurrences. `test:integration:no-ak:sandbox:none`
23 files / 187 tests, core tools+hooks+skills 1333 tests, CLI skill loaders 63
tests, typecheck, eslint and prettier all clean.

* fix(skills): close the six deferred findings from rounds 2-4

Every deferral recorded across rounds 2, 3 and 4, verified with intact/flip
arms and probes on the real classes rather than by reading the diff.

- docs recipe: `$QWEN_SKILL_ROOT` was unquoted. The command string is handed
  to a shell (`bash -c`), so a project path containing a space splits into two
  words: measured `bash: /tmp/My: No such file or directory`, exit 127, which
  is not 2 and therefore fails open — the same shape as #11067, in the recipe
  that teaches the fix. Quote it, and say why.

- docs: an omitted `matcher:` on a tool event is stored as '' and compiled to
  `^$`, which matches no tool name. A probe on a real SessionHooksManager
  confirms it: two hooks registered, only the `*` one matches
  `run_shell_command`. The hook registers and never fires, silently. Say so,
  and point at `*`.

- docs: hook commands run through the platform shell — bash on macOS and
  Linux, Git Bash on Windows when `MSYSTEM`/`TERM` say so, otherwise cmd.exe
  or PowerShell, where `$VAR` is not expanded and a `.sh` is not executable.
  `shell: bash` forces bash but resolves to whatever is on PATH, so the note
  stops short of promising it works on a stock Windows box.

- `BundledSkillLoader`: the load-time `skills.disabled` filter cannot help a
  command that was already built, so a skill disabled afterwards still had its
  side effects applied on invocation. `SkillCommandLoader` has refused this
  since before this PR; mirror it. Pre-existing for `allowedTools`, but this
  PR widened the same call to hooks.

- `packages/core/src/index.ts`: stop exporting `applySkillHooks`. It is the
  one entry point whose caller must apply the folder-trust gate itself, it had
  no consumer outside its own module, and exporting it invites the bypass that
  caused #11067. `applySkillSideEffects` stays exported and applies the gate.

- test: the untrusted-folder warn fired on `allowedTools?.length || hooks`,
  but the only case used a skill carrying both, so dropping the hooks operand
  left all tests green. Pin a hooks-only project skill.

Both new tests are discriminating: removing the bundled enabledness check reds
only the new bundled case (1 failed / 28 passed), and dropping the `|| hooks`
operand reds only the new skill-utils case (1 failed / 23 passed).
`test:integration:no-ak:sandbox:none` 23 files / 187 tests, core
tools+hooks+skills 4588 tests, CLI services 551 tests, eslint and prettier
clean. The Windows shell behaviour is read from `getShellConfiguration`, not
executed — no Windows host was available.
2026-09-06 17:11:57 +00:00