Commit Graph
9706 Commits
Author SHA1 Message Date
易良 351b5fc51a ci(lint): stop new cli imports from the core package root (#11798)
Importing a value from the @qwen-code/qwen-code-core root evaluates all
of core in every test whose import graph reaches the importing file,
which is where most cli test time goes (#10908). The existing rule stops
at packages/core/src, so cli kept adding root imports: 446 production
files at #10957's merge, 465 eight days later.

Enable @typescript-eslint/no-restricted-imports for cli production files
with type-only imports allowed, reusing the barrel specifier set from
no-core-root-barrel-import. The 372 files that value-import the root
today are listed in eslint.legacy-core-barrel-imports.mjs; the list only
shrinks. The new file is registered wherever eslint.legacy-filenames.mjs
is: the lint-gate freshness list, the autofix sensitive-file classifier,
and the hygiene fix agent's deny list.

ESLint and Prettier (--experimental-cli) were run on the changed files,
and the new block was exercised through ESLint's API against the real
eslint.config.js. The scripts/tests vitest suites were not run locally.

Refs #10908
2026-09-14 04:49:03 +00:00
易良 f71a4e797e chore(pnpm): scope build approvals, pin the ACP zod peer, add a freshness lane (#11797)
Follow-ups left open when #11625 merged: its three unresolved review threads,
the deferred finding in #11681, and #10524.

- allowBuilds decisions now honour the whole key the way pnpm does: a bare
  name covers every version, an exact `name@version` (or a `||` union) covers
  only those, and a range or source-like key decides nothing. Reducing a key
  to its name let an approval scoped to one version cover the next (R1-10).
- Pin `@agentclientprotocol/sdk>zod` to 3.25.76 so a pnpm worktree runs the
  zod the npm tree installs. Under zod 4 the SDK's own `instanceof z.ZodError`
  test stops matching and a malformed request answers -32602 instead of
  -32603; both versions are npm-locked, so the agreement gate cannot see the
  split (R1-1).
- Cover web-shell's shipped client sources with the undeclared-import rule;
  it is the one published package whose sources are not under src/. Measured
  cost today: zero violations (R2-6).
- Add a path-filtered workflow that regenerates pnpm-lock.yaml with
  `corepack pnpm import` and fails when the committed file differs, which is
  the freshness half set membership cannot cover (#11681).
- setup-worktree: accept only an executable file on PATH, so a corepack left
  without its exec bit reports the actionable error instead of EACCES
  (#10524).
- Name the esbuild divergence an accepted parity gap, and record what
  `fsevents: false` costs a macOS worktree.
2026-09-14 04:45:19 +00:00
易良andQwen-Coder 32a25efa6e fix(cli): resolve ACP core settings against the active target dir (#11647)
* fix(cli): resolve ACP core settings against the active target dir

The `qwen/settings/getCore` and `qwen/settings/setCoreValue` handlers in
`extMethodInternal` fell back to `process.cwd()` when no explicit `cwd`
was passed. In a worktree-backed ACP session the daemon runs from the
project root, so workspace-scoped settings were written to
`<projectRoot>/.qwen/settings.json` instead of the worktree's own
`.qwen/settings.json`.

Fall back to `config.getTargetDir()` instead, which
`relocateWorkingDirectory()` keeps pinned to the active session
workspace (including worktrees). An explicit `cwd` still wins.

Fixes #8138

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

* fix(cli): resolve ACP settings from the requesting session's target dir

The `qwen/settings/*` + `qwen/permissions/*` handlers resolved their
workspace root from `process.cwd()` or the daemon's bootstrap
`this.config.getTargetDir()`, both of which stay pinned to the boot
workspace. A worktree session relocates *its own* Config, so
workspace-scoped writes landed in `<projectRoot>/.qwen/settings.json`
instead of the worktree's own `.qwen/settings.json` (#8138).

Resolve the workspace root from `session.getConfig().getTargetDir()`
(falling back to the bootstrap Config when the client sends no
`sessionId`) and apply it uniformly across the settings/permissions
surface. An explicit `cwd` still wins.

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

* fix(cli): consolidate duplicate @qwen-code/qwen-code-core imports

Merge ExtensionManager and the type-only Config/GoalSnapshotV2 into the
single existing value import so import/no-duplicates passes under
--max-warnings 0.

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

* fix(cli): resolve getMemoryPaths against the active session target dir

Align qwen/settings/getMemoryPaths with its getMemory/setMemory siblings by resolving through settingsCwdFor, so the project memory file points at the requesting session's worktree rather than the daemon boot dir. Correct the settingsCwdFor doc comment, which overstated the no-sessionId fallback as preserving process.cwd() behaviour.

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

* fix(acp): fail loud on unresolvable sessionId, drop per-request env injection

settingsCwdFor previously treated an unresolvable sessionId as absent,
silently retargeting every qwen/settings/* and qwen/permissions/* read and
write to the daemon's bootstrap workspace (a workspace the caller never
selected, with its own trust decision). Reject an unresolvable id with
errorKind session_not_found instead, keeping the no-sessionId fallback that
the workspace-service setRules path depends on.

The twelve rewired handlers also called loadSettings without options, so a
session-scoped read injected the foreign workspace's .env into the daemon's
process-wide process.env. Route them through a shared loadRequestSettings
that sets skipLoadEnvironment (and states consumeCorruptionEnvVars
explicitly, since an options object replaces loadSettings' = true default).

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

* fix(cli): scope ACP settings adoption and permission sync to the session workspace

Resolve the requesting session's stable workspace root (its bound storage)
instead of the live cwd, so session/cd no longer repoints the twelve settings
handlers at a subdirectory holding no .qwen/settings.json (R2-2). Only publish
a freshly loaded LoadedSettings into the process-wide this.settings cache when
it was loaded for the daemon's own workspace, so a bare sessionId read cannot
repoint the daemon-global consumers (R2-3). Scope syncLivePermissionManagers
fan-out to the workspace a workspace-scoped setRules write landed in, instead
of broadcasting the delta to every live session (R2-5).

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

* fix(cli): honor explicit cwd and reject malformed sessionId in ACP settings resolver

Reorder `settingsCwdFor` so an explicit request `cwd` wins before the
`sessionId` lookup, fixing the R2-1 regression where a request that names
its workspace is rejected over a field the resolver never uses. Distinguish
a present-but-malformed `sessionId` (number/object/empty string) from an
absent one so it fails with `invalidParams` instead of silently retargeting
the bootstrap workspace (R3-3). Restore the lost fallback-branch pin and add
the both-fields and malformed-sessionId cases the review asked for (R3-4).

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

* fix(cli): name and pin the session workspace root, and cover the permission fan-out filter

Extract sessionWorkspaceRoot(config) to give the admission-bound storage
root (storage.getProjectRoot()) a name distinct from the live getTargetDir()
and Config.getProjectRoot(), and route settingsCwdFor and the workspace-scoped
permission fan-out through it. This is a pure refactor of the two inline reads.

Add two regression cases that pin the fan-out filter against the two mutants
that previously survived green: a user-scoped setRules delta must still reach
every live session across workspaces (the Workspace guard must stay
conditional on scope), and the requesting session must survive its own filter
after a session/cd moved its live cwd into a subdirectory (the filter must key
on the stable root, not getTargetDir()).

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

* fix(cli): keep MCP/hook/extension writes on the workspace-global coordinate

The MCP status/apply routes (buildManagedWorkspaceMcpStatus,
reloadWorkspaceMcpDiscovery) and the hooks/extensions status routes resolve
only the bootstrap workspace. Resolving sessionId on the write side made a
handler answer "saved" for a server no route would ever list or apply, so
setMcpServer, removeMcpServer, setHook, removeHook and setExtensionSetting
now resolve only an explicit cwd (R3-2, option b). Session-aware resolution
stays where write and read-back share the coordinate (core settings, memory,
permissions).

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

* fix(cli): canonicalize workspace path spellings in permission fan-out

syncLivePermissionManagers and adoptRequestSettings compared workspace
coordinates with path.resolve only, so a grant revoked under a different
spelling of the same directory (symlinked /tmp vs /private/tmp, or a
different case) never reached the live session's permission manager while
the caller was told it saved — fail-open. Canonicalize both sides with
realpathSync.native (path.resolve fallback) plus a darwin/win32 case-fold.

Also treat a present-but-empty cwd as absent in the five workspace-global
settings handlers (|| instead of ??), matching the session-aware handlers.

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

* style(cli): apply prettier formatting to the ACP agent files

The Prettier lane flags both files touched by this PR (a JSDoc continuation
line and an over-width path.join call), which fails Lint & Static before any
later step runs.

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

* fix(cli): rely on realpath casing instead of folding case in workspace comparison

realpathSync.native() already returns the on-disk casing on macOS and
Windows, so the toLowerCase() on top was redundant on case-insensitive
volumes and actively wrong on case-sensitive ones: proj and PROJ are
genuinely different workspaces there, and folding authorised one under a
rule written for the other. Also pin the removal direction of the
cross-spelling fan-out and clean up the test symlink.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-09-14 03:51:58 +00:00
易良andQwen-Coder 6d7b5e19fc fix(cli): treat write-side PTY EIO as a benign teardown race (#11788)
isExpectedPtyRaceError only tolerated the read side of the pty teardown
race, so a `write EIO` fell through to writeStderrLine + process.exit(1)
and killed the TUI. That is exactly what Ink's throttled log write raises
once the terminal is closed or detached: node delivers it asynchronously
out of the stream write callback, so it reaches the process-level
uncaughtException handler as `Error: write EIO` with code EIO.

Direction does not change the class of the race, so the EIO branch now
matches the write side using the same shape it already used for the read
side. Everything else about the classifier is untouched: the read-side
cases, ioctl EBADF and the resize-after-exit message stay benign, and any
non-pty error (including an EIO whose message names neither read nor
write) is still reported to stderr and still fatal.

Reproduced against a real pty (python pty.openpty master, node child with
stdout/stderr on the slave, then close the master): with origin/main the
child dies with exit code 1 on the first `write EIO`; with this change the
identical error is classified benign and the child keeps running.

EAGAIN is deliberately left read-only. Flooding a pty whose master is never
drained never surfaced a `write EAGAIN` here — libuv queues tty writes
rather than reporting backpressure to JS — so a write-side EAGAIN branch
would be dead code.

Refs #11783


Patrol-Run: qwen-issue-patrol/jmu04jbjw2s

Co-authored-by: Qwen-Coder <[email protected]>
2026-09-14 03:49:42 +00:00
qqqys 60be57673d fix(goal): keep a full-claim checkpoint replay from spending a stall, and tidy batching follow-ups (#11796)
- A restore replay was exempt from the stall breaker only when it failed.
  One that came back with a full claim list on an overflowing window still
  added a stall, so a session restored one stall short of the limit was
  stopped at activation before any turn of its own ran. The success arm now
  keeps the restored streak too, while still recording the diagnostic and
  writing the checkpoint.
- goals.md says the timeout bounds each model call, not the whole check,
  and that a batched check's wall time is charged to goalMaxActiveMinutes.
- The restore-replay tests share one crash-state harness, the batchSizes
  helper states the split rule the code evaluates, and a test pins that a
  pause between batches sends no further batch.
2026-09-14 03:49:33 +00:00
qqqysandqqqys 1291d2a68d fix(dingtalk): remove background agent headings (#11800)
Co-authored-by: qqqys <[email protected]>
2026-09-14 03:46:39 +00:00
易良andQwen-Coder f9534f4395 fix(web-shell): make the cockpit visual capture deterministic (#11786)
Two independent sources of run-to-run nondeterminism landed on
session-workflow-cockpit-*, and neither was visible to the suite.

The artifact dock's 200ms open slide ran live during captures because the
suite renders under prefers-reduced-motion: no-preference, so the CSS opt-out
the app already ships never applied. Measured at the mutation that mounts the
dock: artifactPanelDockOpen is running at currentTime 0 with the dock at
width 0 and the divider at x 1276, settling at width 504 / x 772. No assertion
in any screenshot spec gates on the dock, so whether a capture lands inside
those 200ms is down to how long the pre-capture waits happened to take.

The cockpit also focuses its own back button on mount, and whether Chrome
paints the :focus-visible ring for a programmatic focus is a heuristic. Over
five captures of one unchanged tree the ring appeared in three of the five
light renders and three of the five dark ones; run1 vs run2..5 through
web-shell-visuals-compose.mjs scored CHANGED 0.05% in 4 of 4 pairs, with all
499 differing pixels inside the ring's own box and the button's border and
label byte-identical.

Force reduced motion through use.contextOptions and give the harness a
clearFocus that the cockpit spec calls before its capture. Two details that
are easy to get wrong and are now pinned by visual-capture-contracts.test.ts:

- reducedMotion is a browser.newContext() option, not one of the runner's own
  use options. Written as a bare sibling of viewport it is silently dropped:
  project.use.reducedMotion reads back as 'reduce' while the page still reports
  no-preference (measured on @playwright/test 1.61.1).
- clearFocus stays per-scenario. Called from captureScreenshot it moved 11 of
  68 views, slash-menu-dark by 30.1%, because that menu is open precisely
  because the composer has focus.

After both: 0 of 4 compose pairs changed, dark is byte-identical across 5 runs,
and the full 4-spec render moves 1 of 68 views against the pre-fix render --
session-workflow-cockpit-dark at 0.05%, which is the ring going away.


Patrol-Run: qwen-issue-patrol/jmtzy3u652k

Co-authored-by: Qwen-Coder <[email protected]>
2026-09-14 01:55:50 +00:00
TianyuanandShaojin Wen 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 <[email protected]>
2026-09-14 01:02:42 +00:00
callmeYe 06d65463ad fix(web-shell): move source download to the header (#11641)
* fix(web-shell): move source download to the header

* test(web-shell): require a single source download action
2026-09-14 00:44:32 +00:00
c26a8fd004 feat(core): make the web_search budget configurable and bound the extractor fallback (#11692)
* feat(core): make the web_search budget configurable and bound the extractor fallback

The side request ran under a fixed 60s budget set when the tool was opt-in and rarely opened pages. With web_extractor on and the tool on by default, live searches take 13-107s, so the budget is hit routinely and the partial path hands the model a raw page dump instead of an answer. Add tools.webSearch.timeoutMs (WEB_SEARCH_TIMEOUT_MS), default 120s and at most 600s, and drive the client timeout, the abort signal and the timeout message from it. Salvaged extractor text now carries a label saying it is raw page content rather than the agent's narration, and is cut to 6000 characters.

Also pin two properties the citation policy relies on: evidence bullets are bare URLs and nothing else, and the tool description and the per-result footer derive from one rule list so they cannot drift. The docs and settings schema named qwen3.6-plus as the automatic search model after it changed to qwen3.8-flash.

* docs(cli): state per-tool execution cap precedence over the web search budget (#11692)

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

* fix(cli): reject out-of-range tools.webSearch.timeoutMs at write time (#11692)

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

* fix(cli): qualify the web search timeout partial-result promise (#11692)

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

* test(cli): add web search timeout constants to exhaustive core mocks (#11692)

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

* test(core): cover cancellation winning over partial-result salvage (#11692)

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

---------

Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Qwen Autofix <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: qwen-code-ci-bot <[email protected]>
2026-09-14 00:41:41 +00:00
易良andQwen-Coder 42b7091d1a fix(cli): reap running monitors when the process dies on an uncaught exception (#11742)
* fix(cli): reap running monitors when the process dies on an uncaught exception

Monitors spawn detached so the tool can group-kill them, which also means
they outlive the CLI when it dies: the crashed session can never consume
their terminal events, and the in-memory registry gives a resumed session
no reattach path (#11732). Abort all running monitors from the
uncaught-exception handler so the existing abort path group-kills them
before the process exits.

* fix(cli): record monitor reap failures in the crash debug log

Review feedback: a throwing abortAll would be swallowed by the catch and
the debug lines are written before the reap runs, so the one
distinguishing fact (the reap failed and monitors still leaked) was
dropped. Hoist logPath and append a MONITOR_REAP_FAILED line
best-effort.

* fix(cli): record the reap outcome synchronously and scope the reap comment

- Snapshot the running count before abortAll and append a synchronous
  [MONITOR_REAP] reaped=N line on success, so a crash log can tell
  "reap skipped it" from "signalled but ignored SIGTERM" (R1-2).
- Narrow the reap comment: the handler covers the Config it was installed
  with; per-session ACP registries need process-wide tracking, which stays
  follow-up work (R1-1).

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-09-13 21:59:40 +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
Shaojin Wenandprobe 8520dfc1d9 docs: translate permission card and browser notification designs (#11775)
Co-authored-by: probe <probe@local>
2026-09-13 21:27:10 +00:00
Shaojin Wen 0fedceba20 fix(review): document the deadline's pause ceilings and improve capture-local errors (#11779)
Three follow-ups to #11686, from its sandboxed verification report and the
audits of this change.

The plan-recorded wall is elapsed time and the next reverse-audit round is
priced at the costliest admission-to-admission span, so a pause is charged
twice: it spends the wall and, while it is the longest gap, prices the next
round. A new Review Deadline section in the user docs gives the ceilings:
under the default reserve a review can pause after its first audit pair
(rounds 1 and 2 launch together) for at most about 3h20m / 5h20m / 7h20m on
the 8h / 12h / 16h walls, (wall - 80m) / 2 on any wall of 4h or more, less by
half of any time before the pair launched. It also says a stopped review
composes and reports what it proved with the verdict capped, that running
/review again follows the incremental-review rules rather than continuing the
audit, and that setting the wall through the environment applies to every
review in the session. DESIGN.md states the same rule, including the gate's
price floors. The soft-deadline paragraph under Headless runs and the
--deadline help point to the section, and the help now names what the
environment epoch wins over. Tests drive the real gate to pin the ceilings,
the later-launch discount and the costliest-span case.

capture-local keeps #11686's one-line error contract (a usage error exits 2,
anything else 1) and now prints the error's stack after that line under
--debug, so an internal fault can be located without turning operator errors
back into crash banners. Only the flag does this; debug variables in the
environment do not. The handler also rules --out before the tree is captured,
as fetch-diff and issue-context do: a directory --out used to run the whole
capture and then fail with EISDIR, and a blank one wrote the plan to a
whitespace-named file.

The shared assertWritableOutPath now names the mistake for a non-string
--out: "--out must be given once, as a file path" for a repeated flag (an
array), "--out must name a file path" for --no-out, instead of
`out.trim is not a function`. fetch-diff and issue-context pass --out through
instead of String()-ing it, which wrote a repeated flag to one comma path and
--no-out to a file named `false`; both now exit 2 before fetching.
2026-09-13 21:25:08 +00:00
Shaojin Wen faa395885e feat(web-shell): Improve context overview and add manual compression (#11700)
* feat(web-shell): Improve context overview and add manual compression

* fix(web-shell): Keep context compression state consistent

* fix(web-shell): Clarify context compression state hints

* fix(web-shell): Preserve cold-session context usage

* fix(web-shell): Guard context refresh and compression lifecycle
2026-09-13 14:39:21 +00:00
6a0806faf3 fix(core): let the producer's own budget decide shell output size (#11727)
* fix(core): let the producer's own budget decide shell output size

Shell truncates at its declared budget (30k by default) and keeps a
head-and-tail preview so the trailing exit and error summary survives. The
scheduler then re-bounded that body against the global threshold plus headroom
(~28k by default), which sits BELOW the tool's budget — so output in that window
was reduced to a short head instead, losing the trailing status that larger
output keeps.

Producers now report `outputBudgetApplied` when they have sized a body against
their own budget, including when nothing was cut, and the generic gate stands
down for it. Spawn failures, background launches, and separately built error
messages are unmarked and keep the gate. Per-tool, combined, and aggregate batch
budgets are unchanged.

Also caps the attribution warning's exception text, which is appended outside
the truncation envelope and was the one unbounded input the gate had been
covering on the failure paths.

* fix(core): bound the marked timeout detail and reserve appended Shell metadata

Review follow-up on the output-budget marker: the scheduler's timeout
branch stood the generic gate down for a marked body with no bound
behind it, and Shell set the marker before appending the long-run
advisory and attribution warning, so the assembled string could cross
the declared budget after being marked.

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

* fix(core): clamp the reserved Shell body budget so the marked pass cannot stand down

* chore: trigger GitHub pull-ref resync (empty)

* fix(core): share the truncation persistence tri-state and unify Shell metadata appends

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

* fix(core): cap the Shell metadata reservation at half the explicit threshold

At a sub-advisory explicit truncateToolOutputThreshold the reserved
long-run advisory consumed the whole body budget, leaving a 1-char
head-and-tail preview that dropped the trailing exit-code line.

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

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

---------

Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
2026-09-13 14:26:23 +00:00
01aa4f267f feat(web-shell): make Session Workflow dependencies navigable and quiet its chrome (#10938)
* fix(web-shell): keep the auto-sized dialog's gutter on a phone

The `auto` size floors the panel at `min-w-[min(100%,560px)]` so a small plan
graph does not collapse to a narrow column. tailwind-merge keeps that class
alongside the base ceiling `max-w-[calc(100%-2rem)]` — different modifier
groups — and below the `sm:` breakpoint min-width wins over max-width. On a
390px viewport the Plan & tasks dialog therefore rendered its rounded, ringed
panel flush to both screen edges, while every fixed size (sm/md/lg/xl) kept
the 1rem gutter at that same width.

Floor the panel at the gutter the ceiling already reserves.

* fix(web-shell): bind the DAG lane reservation to the constant that places lanes

Lane spacing was stated twice in two languages: `EDGE_LANE_HEIGHT` in TS,
which places each layer-spanning return lane at `lane * EDGE_LANE_HEIGHT`,
and a literal `9px` in `.dagCanvas`'s bottom padding, which reserves room for
them. The two are coupled only through the `--plan-edge-lanes` inline style,
so raising the constant alone leaves the reservation short; with enough
spanning edges the clamp pins the outer lanes together and layer-spanning
edges overlap again — the exact defect the lanes exist to fix. Nothing would
have caught it: the lane test mocks `offsetHeight` directly.

TS now publishes the pitch as `--plan-edge-lane-height` beside the lane count,
and the stylesheet reads it.

Also give the graph a phone: three 240px lanes plus two 64px gutters are
~850px, so the canvas scrolled in both axes at once and the horizontal scroll
hid the layer the vertical scroll was looking for. Below 720px the lane and
gutter narrow. Layer semantics and the rect-measured edge routing are
untouched, so this changes what fits, not what is drawn.

The new stylesheet guard pins both, in the shape the other `.css.test.ts`
files use, since jsdom does not compute the cascade.

One test-only addition on the same component: the spanning-edge test fixed
the edge's arrival column (`H 876`) but not its arrival row, and nothing else
in the file asserted a spanning edge's `endY`. Substituting the return lane's
`routeY` for `endY` in the path tail draws the edge ~170px below the target
node while every existing assertion still passes. Pinned corner-agnostically.

Refs #10866.

* test(sdk): pin the skills entry in the transcript compactor keep-set

`compactTaskExecutionOutput` keeps an allowlist of `task_execution` fields
when it drops the bulky ones, and every neighbouring entry — `executionSummary`,
`tokenCount` — is already pinned by the reducer tests. `skills` was not:
removing it from the keep-set left the whole suite green, and a session
restored from a persisted transcript would silently lose the skill list the
live run recorded.

Mutation-checked: deleting `'skills'` from the keep-set fails exactly this
assertion (1 failed | 327 passed); restored, 328/328.

* feat(web-shell): make workflow dependencies navigable, and let the surface state each thing once

Two related passes over the Session Workflow inspector and cockpit.

## Dependencies become navigation

A step's dependencies rendered as `todo.blockedBy!.join(', ')` — a run of raw
Todo ids, in both the graph node and the inspector detail. An id is an
address, not a label, so following an edge meant reading `step-3, step-7` and
then finding those rows by eye, in a panel whose entire subject is which step
waits on which.

Each reference is now the step it points at — the step number the list and
the graph both already show, plus its title — as a control that selects that
step. The dependency list becomes the graph's navigation.

Recent activity was sliced to 6 while the count beside it reported the true
total, so the panel contradicted itself and the remainder was unreachable.
The cap stays as a preview and gains a control that opens the rest.

## The surface states each thing once

The panel had accumulated chrome that competed with its content:

- Section labels were tracked-out uppercase at 10-11px, so every fixed label
  shouted over the step title and status it introduced. They are labels, not
  headlines: sentence case at a muted weight.
- The cockpit spent the accent colour on a fixed "Dependency graph" eyebrow
  above the session title — making the one blue thing on the page the one
  thing that never changes, while the graph below uses that same accent to
  mean "this node is selected". The label moves to the subtitle line and the
  accent is left to mean selection.
- Completed-over-total sat in a tinted pill directly above the progress bar
  that draws the same ratio, beside the status word that names the same
  state — three surfaces for one fact. The count keeps the tone as ink and
  drops the pill.
- Runtime, tool calls, tokens and artifact kind/status were glued into single
  strings with middle dots, which wrapped as a unit and read out as one run of
  text. They are separate facts, so they are separate elements.

The identity line keeps the mono face only on the short session id, which is
an address the user copies or matches against a daemon log.

No token or theme values change; this is the existing system applied more
sparingly.

Both behaviour changes are mutation-checked: reverting the dependency list to
the joined id string, and restoring the silent cap, each fail exactly their
new test.

Refs #10866.

* docs(core): say why the Todo dependency semantics are not behind the flag

`todo_write`'s omitted-`blockedBy` preservation, and the `blockedBy` schema
text that invites the omission, both reach sessions with Session Workflow
disabled. That was a deliberate choice when the behaviour landed, but nothing
in the file said so, so it reads as a missed gate — the `hasActivePlan`
derivation sat bare and the only nearby comment explains the dangling-edge
filter, not the gating.

Record the decision and its reasoning: these are plan data-model semantics
rather than presentation, and gating them on a visualization switch would
give the tool two dialects, storing different plans for the same call
depending on whether anyone is looking at the graph.

Record the blast radius in the same place, because "reaches every session"
overstates it. The merge changes an update only when the previous plan still
has an unfinished item, the same id carried a non-empty `blockedBy`, and this
call omits it. Plans that never used dependencies are untouched
(`undefined?.filter()` is `undefined`), callers that always send the full list
are unaffected, and `[]` stays the explicit clear. What does reach every
session is the schema text, which is why it is named as widening this
branch's exposure rather than as an independent change.

Comment-only; no behaviour, no signatures.

* feat(web-shell): lead the plan DAG with the step, not its status

Read against the three questions a plan graph is opened with — what is
running, what is blocked on what, what needs me — the node answered none of
them first.

**Reading order was inverted.** The node led with a status glyph, then the raw
Todo id, then the status word; `nodeContent`, the only element that says what
the step *is*, came third. At graph scale a reader scanned `● step-3 Blocked`
before "Compare findings". An id is an address and the least useful thing at
that scale, so it leaves the node face entirely — it stays in the detail panel,
where it is actually copied. Content leads, at 13px/500, behind the step
number the inspector list and dependency chips already use, so the three
surfaces address a step identically instead of three different ways.

**Status was stated three or four times per node** — glyph, status word,
border tint, plus an attention badge. It is now stated once, as a 3px left
rule, and only when it is worth saying: waiting is the resting state of most
nodes in a real plan and carries no rule. The stylesheet already made this
argument for `blocked`; extending it consistently is what stops the canvas
reading as a wall of tinted boxes and gives the one running node something to
stand out against. Attention folds into the same rule and outranks the status
tone, because it is the state the reader is being asked to act on.

Status is colour, which assistive tech cannot read, so it stays in the
accessibility tree as words.

**Time was invisible** in an execution trace. The node face now carries the
agent count and the summed agent runtime for that step — the "is this alive or
hung" signal, which is the first thing asked of a live run. Two facts with
real spacing, not one middle-dot-joined string that wraps as a unit and reads
out as a single run of text.

**Dependencies were stated twice**, as a drawn edge and again as
`Depends on step-3, step-7`. The row is now rendered only when no edge states
it — above `MAX_RENDERED_PLAN_EDGES` the graph draws none, and there the row
is the dependency's only statement, so deleting it outright would have lost
information in exactly the case that needs it most. It renders as the same
number-and-title chips the inspector uses.

The input port dot goes with them: the left edge now carries the status rule,
and an incoming edge already terminates in an arrowhead at the node — that
arrowhead is the input marker. The output port stays, since outgoing edges
leave their source unmarked. Both port dots also drop `--agent-blue-500`;
every node with an edge had one, so a permanently blue dot across most of the
graph left selection no colour of its own.

Edge routing, the layering algorithm, the return lanes and the measure pass
are untouched. This is the node face and the colour rule.

Three geometry tests identified a node by its first `<span>` — the Todo id.
That is now the accessible status word, so they bind to `data-plan-node-id`
instead, which is the stable hook and was always the better one.

Every behavioural change is mutation-checked: ungating the dependency row,
deleting the accessible status word, and dropping the elapsed each fail
exactly their own assertion.

Design plan, including the two revisions the review pass forced, is in
docs/plans/2026-09-04-session-workflow-dag-design.md.

Refs #10866.

* fix(web-shell): drop the unused dependencyIdsByTodo destructure

ESLint flags the graph destructure binding as never used; the memo
still computes and returns the map for other consumers.

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

* fix(web-shell): stop the activity grid from swallowing the show-all control

Round-1 review findings.

**Blocker.** `.showAllActivity` was a single class (0,1,0), but the button is
a direct child of `.activityList`, whose base rule `.activityList > button`
(0,1,1) declares the four-column activity grid and wins. The label — a bare
text run that Grid wraps into an anonymous item — auto-placed into the 52px
first column and wrapped to three or four lines with three empty columns
beside it, and the intended padding lost too. Hover only looked right by
coincidence, because the row's own hover set the same background.

This is the collision handled two rules earlier for `.metrics`, missed here.
Both are now pinned by the stylesheet guard so neither can regress to a bare
class, and the fix resets `display` and `grid-template-columns` explicitly
rather than relying on source order.

**The graph's step-details panel still joined raw ids.** The node face's
chips cannot be controls — that row lives inside the node's own `<button>`
and a nested button is invalid HTML — but this panel sits outside it, so its
references become real controls that select the step they name, which is what
makes a dependency list navigation rather than text. `Depends on` and
`Unblocks` both.

**The narrow-viewport claim was wider than the fix.** Three 168px lanes plus
two 32px gutters is ~588px, which removes horizontal scrolling between there
and the 720px breakpoint but still scrolls on a 390px phone. Added a second
step below 480px that gets a three-layer plan under a phone viewport, and
said plainly in the comment that deeper plans still scroll — reusing the flat
layout stays with #10866. The guard test is renamed to what it pins: jsdom
computes no layout, so it can assert the two steps exist and narrow, not an
outcome.

**The lane pitch had a second copy.** The `var()` fallback restated `9px`
next to the `.dagCanvas` default, so "TS owns the pitch" held only when the
graph has dependencies. The default is now `0px` — the zero-lane case
collapses the reservation regardless — and the guard rejects the fallback
copy too.

**Comment antecedent.** "the prompt change" in todoWrite named nothing a
later reader could find; it now quotes the schema description it means and
says why a tool schema reaches every session.

Refs #10866.

* fix(web-shell): keep the node's non-colour status channel, and stop clipping its port

Two defects from my own review of the DAG pass, both in the class the round-1
review already caught once: invisible to jsdom, invisible to a passing unit
run, and only findable by reading the stylesheet.

**The status glyph was an accessibility channel, not decoration.** Making the
left rule the single carrier of status dropped the glyph — but the constant
it came from says exactly why it exists: "Status is carried by a glyph as well
as a colour so the graph survives colour-blindness, high-contrast mode, and a
greyscale screenshot." Colour plus a screen-reader-only word leaves a sighted
colour-blind reader, high-contrast mode, and any greyscale capture unable to
tell running from completed from needs-attention. "One status, one place" was
the wrong rule; the right one is one carrier of the status *colour*, plus a
redundant non-colour channel. The glyph returns for every status, in the meta
line so the step's content still leads, and muted so the rule stays the only
thing carrying the tone. Pinned per status, and pinned as distinct from each
other, so the shape channel cannot silently collapse.

**The status rule clipped the outgoing port.** Drawing it as `.node::before`
needed `overflow: hidden` for the node's 8px radius to apply to it — which
also clips the port dot at `right: -4px`, which is deliberately outside the
box. It is a `border-left` instead: border-radius clips it natively, no
overflow, and a transparent rule paints the node's own background so a resting
node reads as 3px of extra left padding with no width difference between
states. The stylesheet guard now rejects the overflow, the pseudo-element and
a missing port offset together, since the three only fail as a set.

Also drops `.nodeGlyph`'s dead sibling state: the rule is back in use, so
nothing here is unused CSS.

The design plan records both as corrections rather than folding them in, so a
later reader sees which of its rules were wrong and why.

Refs #10866.

* fix(web-shell): put dependency truncation where it can actually fire

Round-2 review findings that survive at this head; the round's three blockers
(the node's clipped port, the show-all specificity collision, and the
step-details panel's raw ids) were already fixed in the two commits after the
head it reviewed.

**The chips' `text-overflow: ellipsis` was dead.** It was declared on the
`inline-flex` chip itself, but `text-overflow` applies to a block container's
own inline content — the title sat in an anonymous flex item, so a long
dependency title was clipped hard with no ellipsis. The title now has its own
span carrying `overflow`, `min-width: 0` and the ellipsis, in all three places
that render a reference (the node face's read-only chips and both directions
of the step-details panel's links), and the stylesheet guard pins that it
lives on the title rather than the box.

**The comment above `.node` still described the old status behaviour** — it
said status drives the accent that drives the border, which this branch
stopped doing two commits ago. Rewritten to describe the border/rule split,
keeping the hazard it originally recorded: with status off the border, a
selected completed or blocked node can no longer pick up a second border in a
clashing hue.

**The compaction test's comment overclaimed.** It said every other keep-set
entry is pinned by that test; it pins `skills`. Narrowed to what it does.

Two coverage gaps the same round named:

- The inspector test exercised only the upstream direction. Upstream and
  downstream render through one helper but read from different sources
  (`blockedBy` versus the projection's `dependentsByTodo`), so covering one
  did not cover the other; the downstream link is now rendered, asserted and
  clicked.
- `planExecution.agentCount`'s singular and plural branches are separate
  formatter paths, and only the singular one was exercised — `1 agents` would
  have shipped green. Both are now pinned.

Refs #10866.

* fix(web-shell): drop ghost dependency controls and mark them interactive

The step-details dependency buttons mapped raw selectedTodo.blockedBy,
so an unresolvable step id rendered as a clickable "? <id>" control;
clicking it selected a ghost id, emptied selectedTodo, and hid the
details panel mid-navigation. Render from the same filtered projection
the edges draw from (dependencyIdsByTodo: unknown ids, duplicates, and
self-references dropped) and hide the section when nothing resolvable
remains.

Also add the data-plan-interactive marker to both step-details
dependency button groups. The host keyboard handlers in ToolApproval
and TasksStatusMessage isolate plan controls through that marker, and
these were the only interactive controls in the view without it.

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

* fix(web-shell): let dependency titles keep their colour, and attention its ring

The muted `> span` rules match every direct-child span of the chip and
link — now including the title span the truncation fix added — so every
dependency title rendered muted while the chip's own
`color: var(--foreground)` applied to no rendered text. The title now
states its own colour on the title rule, where two classes outspecific
the one-class-plus-element mute.

The attention rule sat before the blocked/ready rule at equal
specificity, so a blocked node that also needs attention wore the muted
ring instead of the attention tone. Attention is declared after the
status tones so it outranks them, as its comment says. Both shapes are
pinned at the source in PlanExecutionView.css.test.ts.

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

* fix(web-shell): dedup repeated blockedBy ids before rendering references

The node-face chip row and the inspector's upstream list mapped raw
model-authored blockedBy, so a repeated id emitted duplicate React keys
and a duplicate chip or link. Every other blockedBy consumer already
dedups with new Set, and the step-details panel reads the deduped
topology projection; both surfaces now match. The dense-plan test feeds
a repeated id and pins the single chip, and a new inspector test pins
the single upstream link.

Also pin the styles.dependencyTitle wiring on the chip and link title
spans in the existing render tests: the truncation rule targets that
class, so dropping the className would re-clip titles without an
ellipsis while every text assertion stayed green.

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

* fix(web-shell): state the 480px step's true plan width in the comment

The comment claimed the 480px step gets a three-layer plan under a phone
viewport, but its own pinned values sum to ~396px (3×116px lanes, 2×18px
gaps, 2×6px canvas padding) — still ~6px wider than the 390px phone the
sibling comment names as the reference. Restate the comment with the true
figure: fits phones wider than ~396px, a 390px phone still scrolls the last
~6px. Comment only; the pinned values and the stylesheet guard are
unchanged.

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

* docs(plans): mark the session-workflow DAG design implemented, resolve its open questions

The doc shipped in this PR declared "Status: proposal, not implemented" while
the same PR ships the implementation, and left two open questions the code
had already answered. Mark it implemented and record the decisions where
they were made: the elapsed count renders on the node face (meta line, when
greater than zero), and attention folds into the left rule via
data-attention, declared after every status tone so it wins the cascade.
The verification note stays open — it is about process, not a design
decision.

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

* test(web-shell): pin the declarations the plan guards actually rest on

The truncation guard now pins `overflow: hidden` beside
`text-overflow: ellipsis` — the former is what makes the latter take
effect, and `overflow` is not inherited from the chip. The two
phone-width steps pin the lane width on its owner selector and add the
paired gap/padding narrowing the fit rests on; dropping any of them
restored the clipped-title or two-axis-scroll defect while the suite
stayed green.

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

* fix(web-shell): drop self-blocks in the inspector, keep status words beside attention

The inspector's upstream dependency filter kept self-references: a todo
listing its own id in blockedBy rendered a "Blocked by" chip pointing at
itself, while the projection's dependentsByTodo builder and the DAG both
drop self-blocks. Apply the same rule so the inspector agrees with every
sibling surface.

The node face's sr-only span also announced attention instead of the
status word; an attention node lost its status from the accessibility
tree while the aria-hidden glyph still carried it for sighted users.
Announce both: the status word first, attention appended.

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

* test(web-shell): witness the downstream click, attention word and cascade order

- click the downstream 'Unblocks' reference in the step-details panel and
  assert the named node gains aria-pressed, so a dropped onClick in the
  selectedDependents block turns red
- render a node into attention and assert its sr-only span carries the
  status word beside "Needs attention"; pin the inspector's self-block
  exclusion beside the dedup test
- extend the cascade-order guard to all six status selectors, not just
  blocked, so regrouping any status tone below the attention rule fails

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

* test(web-shell): pin the plural agent count and the attention attribute

- link two root Agent executions to one node and assert the face renders
  '2 agents', so an EN template that always emits `${count} agent` turns
  red; the count-1 side stays pinned by the existing assertion pair
- assert the attention fixture's article carries data-attention='true',
  the hook the `.node[data-attention='true']` colour rule selects on;
  the 'Needs attention' word was pinned at 18f9fa07, the attribute was not

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

* test(web-shell): clear every .node-subject rule of overflow clipping

The unclipped-port guard only inspected the bare `.node {}` rule, so an
`overflow: hidden` on any of the other `.node`-targeting rules — a status
variant, attention, selection, the `:has()` state, the media-scoped copies —
would still cut the `right: -4px` output port and ship green. Walk every
rule block (at-rule preludes unwrapped) and assert no selector whose
subject is `.node` carries overflow clipping; descendant-subject rules and
the legitimate chip/title clipping stay out of scope.

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

* docs(plans): state the shipped color mappings in the Color table

The table claimed `--border` for every dependency line, `--status-running-fg`
for running "and nothing else", and `--status-attention-fg` for needs
attention "and nothing else". The shipped stylesheet draws edges with
`--muted-foreground` and `--agent-blue-500` arrowheads, dresses in_progress
in the running token, and dresses paused plus the step-details chips in the
attention token. Record the mappings as shipped so a later palette retune
reads the exclusivity clauses as they actually hold.

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

* fix(web-shell): close the cockpit review's rendering and a11y blockers (#10938)

- Keep selection off the status rule: re-pin border-left-color to
  --node-rule in .node[data-selected='true'] so selecting a blocked/ready
  node no longer paints a 3px muted status bar.
- Carry data-plan-interactive on the inspector's dependency links and the
  show-all-activity button, so the transcript's window-level keydown
  handlers early-return while they hold focus.
- Let .dependencyList > li shrink (min-width: 0) so the dependency chip
  title's ellipsis actually engages instead of overflowing the column.
- State dependencies as text whenever neither the aria-hidden edges nor
  the step-details panel can: above the edge budget, in the cockpit
  (showStepDetails={false}) and in document mode; drop self-references
  from the node-face chips like the topology builder does.
- Give attention a shape channel: a muted visible sigil in the node meta
  row, since on paused nodes the data-attention rule re-declares the very
  token paused already wears and colour alone cannot tell them apart.
- Count agents, not bare executions, in the node-face tally (nested
  subagents included; non-agent todo_id tools hidden), matching the
  node's own execution rows and the inspector's Subagents list.
- Scope the visuals spec's runtime gate to the node face and the
  inspector detail separately; the page-wide locator matched both and
  threw a Playwright strict-mode violation.

* fix(web-shell): keep plan arrowheads forward and name blockers (#10938)

Two regressions wenshao measured in Chromium at this head, both introduced
by this diff.

R6-3: the 32px and 18px gutters are narrower than the edge router s fixed
24px shoulder, so the control point landed on the end point (32px tier, end
tangent 0,0) or past it (18px tier, end tangent -14,0). orient=auto then
flips the arrowhead back at its source, and the input port is gone, so the
last direction cue pointed the wrong way at phone widths. The shoulder is now
half the run: every gutter wide enough to afford the 24px floor already
resolved to run / 2, so wide lanes keep their exact curve while the end
tangent stays strictly positive at all three tiers.

R7-3: with edges drawn and the details panel on, the node s accessible name
stopped naming its blockers, because drawn edges are aria-hidden and the
visible chip row is off there by design. A screen reader user had to activate
every node to learn what blocks it. An sr-only summary on the same channel as
the status word restores the words, using the chips step-number-plus-title
labels, without bringing the visible row back.

Evidence is unit level, not a browser capture: the geometry test reproduces
wenshao s measured tangents exactly (0 at the 32px gutter, -14 at the 18px
gutter) and goes red when the floor is restored; the accessible-name test
goes red when the sr-only summary is dropped.
vitest run PlanExecutionView + workflow: 9 files, 84 tests (was 81).

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

* fix(web-shell): count node-face agents from the same sources the rows render

R8-2: agentCount read only transcript subTools, while renderExecution also
renders live child tasks from the task index, so a live child with no
transcript entry rendered a row the tally never counted. Count the same
deduped union the rows come from.

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

* fix(web-shell): keep layer-skipping plan lanes out of the step they pass (#10938)

wenshao's round-2 verification names two merge conditions on 5f70a13.

At <=480px the router gave every layer-skipping edge a fixed 24px shoulder.
The 18px gutter leaves a 10px run, so the vertical segment landed inside the
intervening lane: a dependency from step 3 to step 5 read as one into step 4,
and the SVG paints under the nodes so the lane just disappeared into it.
Derive each shoulder from the gutter measured on that side, the way the
adjacent-layer edge already does, and halve the corner radius so the run into
the arrowhead keeps a positive length. The 64px desktop tier has a 56px run,
so min(24, 28) is still 24 and every desktop path stays byte-identical; the
new witness pins the exact desktop path string.

The node-face agent tally dedups by toolUseId exactly as the rows do, but
nothing exercised the overlap. Add a fixture where a live child task and a
transcript subTool share one toolUseId and assert it counts once (2 agents
beside 2 rows); dropping the filter reads 3 agents.

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

* ci(web-shell): re-fire the head CI lanes that never scheduled

The Qwen Code CI, Web-shell Visuals and review-lane runs created for d91a0f7e at 2026-09-13T08:50:29Z never scheduled a single job: a repo-wide Actions queue jam in that window left 59 runs stuck at status=queued with zero jobs, and both cancel and rerun are refused on them (cancel reports 'completed', rerun reports 'already running'). Runs created after ~09:30Z schedule normally, so these are dead rather than slow.

No content change. This empty commit only re-fires pull_request:synchronize so the lanes actually run on the exact tree wenshao validated in round 3 ('merge once CI has actually run green on this head').

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

Patrol-Run: qwen-pr-closeout/jmtzq8wyy28

---------

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
2026-09-13 13:32:32 +00:00
顾盼 6a211b4652 chore(release): bump node repl to 0.1.4 (#11771) cua-driver-rs-v0.20.6 2026-09-13 10:38:51 +00:00
Shaojin Wen eb780062ca feat(review): record a default deadline in the plan so every run has a wall (#11686)
* feat(review): record a default deadline in the plan so every run has a wall

The reverse-audit loop's deadline gate only ever read
QWEN_REVIEW_DEADLINE_EPOCH, which CI exports and nothing else does, so a local
run whose loop had stopped converging had no bound but the round cap — and the
cap counts rounds, not time.

The capture commands (fetch-pr, capture-local, plan-diff) now record a wall in
the plan: `--deadline <minutes>`, or by default 8h on a 3A diff, 12h on a 3B
one and 16h when huge; `--deadline none` records nothing. It is stored as a
duration from the attempt's start (`deadlineSeconds`) with its source
(`deadlineSource`), because `--resume` never rewrites the plan; the readers add
it to the current attempt's start — the run-session ledger's entry for this
session, else the plan's mtime — so a resume from a new session renews it.
Precedence in the gates is env > plan wall > none. A plan-sourced wall keeps a
third of itself as the tail reserve, capped at 4800s and floored at the
compose floor.

The default is a liveness bound, not a cost calibration, and it does not flip
the huge tier's round reduction (5 -> 3): only an explicit clock — the env
epoch or the flag — does, so a local huge review keeps its five rounds. The
defaults are sized above a healthy run at the cap from the repository's own CI
review runs: un-gated 3A runs post at 106-293 min, posted 3B runs past ~4.5h
are all gate-stopped between rounds 3 and 5 (two more killed at 6h with
nothing posted), and huge runs reach the 6h wall — roughly twice each tier's
CI budget.

A malformed, blank or non-string --deadline is a usage error refused before
any fetch or capture work; a --deadline on a resumed run is reported as
ignored. Malformed env values fall through to the plan wall; malformed plan
fields fail open. Docs (SKILL.md, DESIGN.md, code-review.md, settings.md)
describe the precedence, the defaults and the measurements.

* fix(review): refuse a --deadline that cannot admit round 1, and settle the triage notes

- `parseDeadlineOption` now takes the environment and refuses, up front, a
  wall shorter than the round-1 estimate plus the reserve it would keep
  (`RESERVE_ENV` when set, else `planReserveSeconds`) — `--deadline 10` used
  to parse cleanly and then refuse everything it was asked to build.
- `admitReverseAuditRound` carries the parsed report to the gate instead of
  re-reading the plan from disk on every admission; both callers held it.
- The reserve floor wording says what holds at the boundary: a floored plan
  wall's reserve EQUALS the compose floor, and the round gate still fires
  first because it prices the round on top — pinned by a test.
- DESIGN.md's operator-ceiling passage no longer says a larger ceiling "has no
  expression": `--deadline` states one, and the tier still reads only whether
  an explicit clock exists (#9245). The new section names the wall's audience
  (unattended runs).

* fix(review): price the --deadline floor at a convergence, and settle the review's twenty threads

Two Criticals and eighteen suggestions from the deep review of #11686:

- The up-front `--deadline` floor admitted exactly the wall its own message
  named as sufficient (50 min captured cleanly, then round 1 was refused one
  second later) and priced one round where the loop's convergence pair can
  cost two. It now prices two rounds at the estimate plus the reserve the
  wall implies, refuses at or below that (at or under 90 minutes with the
  default reserve), and is env-free — the same ruling in every shell — so
  the writer tests no longer depend on `QWEN_REVIEW_DEADLINE_RESERVE_SECONDS`
  being unset, and a CI-clocked capture is not aborted over a bound the env
  epoch would have overridden.
- `planReserveSeconds` floors at the EFFECTIVE compose floor, so an operator
  raising `QWEN_REVIEW_DEADLINE_COMPOSE_FLOOR_SECONDS` cannot make the round
  gate admit a round whose verification the verify gate would refuse.
- The `/review` parser consumes `--deadline` with its value and warns, so
  `/review --deadline 90` no longer retargets the review to PR #90.
- The resume note reads the plan and says whether a wall is in force.
- check-coverage exempts a round-cap not-built gap when the plan's wall has
  closed: the round-1 rebuild it would prescribe cannot run.
- `ResolvedDeadline` drops its two unread fields; `hasReviewDeadline` is the
  one definition of "explicit".
- Docs and glosses: the non-converging-run wording in three places, the
  huge-tier predicate at seven stale sites, the DESIGN.md citation that no
  longer existed, the multiplier and admission-horizon numbers, the
  tier-priced-at-capture and `--deadline none`-under-env caveats, and the
  three qualifications the user doc's wall paragraph lacked (idle time is
  charged, the floor is arithmetic only, the workflow allowance is env-only).
- Tests: the lease and gh legs of the early-refusal order, the default wall
  written under an env clock, a real short flag wall for the reserve-scaling
  case, the effective-floor ordering, and the closed-wall exemption.

* fix(review): settle the second review round on the deadline floor and parser arm

- The too-short refusal names two true numbers: what the rejected wall
  would need (its own reserve, which grows with it) and the shortest wall
  the rule admits, found as a fixed point (`shortestDeadlineMinutes`, 91
  under the default rule) instead of a number derived from the input.
- `captureDeadline` prices the flag a second time with the shell's reserve
  and compose-floor overrides — the gate's own expression — and refuses a
  wall the gate would refuse at zero elapsed; skipped under an env epoch,
  which makes the flag inert. `parseDeadlineOption` stays env-free.
- The `/review` parser consumes the token after `--deadline` only when it
  can be a deadline (minutes or `none`), so a PR URL stays the target, and
  the warning names the consumed token instead of predicting the wall.
- coverage's closed-wall exemption gains its open-wall oracle; the gate's
  comment states the open-span worst case an idle gap is charged as.
- Docs: the round-cap reader's band in both directions (honoured inside
  [3, tier], lowered under a clock, replaced outside), `qwen review run`'s
  kill as the second env-only reader, the multiplier's provenance
  (measured for 3A, projected for 3B and huge), the flag's second bar under
  overrides, and the test comments' boundaries.

* fix(review): settle four reverse-audit rounds on the deadline flag and parser arm

Findings from four rounds of independent reverse audit, each with executed
probes, on the earlier review fixes:

- `validateDeadlineFlag(env, raw)` is the one capture-time check — grammar,
  the env-free convergence floor, then the same floor priced with the
  shell's reserve and compose-floor overrides (the gate's own expression),
  skipped under an env epoch. The three capture commands run it up front,
  before any fetch, lease or planning work; the second bar used to reach
  `captureDeadline` only, after that work was spent.
- The refusal message names which bar refused, formats the reserve so the
  minutes add up (floored to a tenth), and cites the shortest wall the
  shell accepts under BOTH bars (`shortestDeadlineMinutes`, now a fixed
  point over the combined rule, `null` when an override puts every wall
  under a day out of reach). A lowered reserve no longer sends the caller
  to a wall the default rule then refuses; under an epoch the figure is
  the default rule's.
- `fetch-pr --resume` is held only to the grammar and the default rule up
  front (the flag is ignored on a resumed plan); a resume that falls
  through to a fresh review meets the shell bar before the stale worktree
  is destroyed and before the fallthrough is announced.
- The `/review` parser routes a non-deadline value after `--deadline`
  through the same disposal pool as an invalid `--effort` value, with the
  `=` form mirroring the siblings (PR-shaped → pool, else reported), and
  its warnings say what happened to the value.
- check-coverage prices the round-cap exemption's rebuild the way the gate
  prices the plain `--round 1` build (from the round stamps, one auditor),
  not at a flat constant.
- The resume note formats minutes, distinguishes a flag wall from a
  default one, and mentions an exported epoch.
- Tests isolate `QWEN_REVIEW_DEADLINE_EPOCH` where the shell bar is under
  test (this repository's own review job exports it), pin the fallthrough
  order, the combined-rule figure, the `=` form, and put the stamped
  compose fixture on the plain pricing branch with ten seconds of slack.

* fix(review): settle six audit rounds on the resumed wall, the FIX waiver and the capture handlers

Round pricing and the resume note:
- describeResumedWall describes what the gates will do, never what to do
  instead: the orchestrator reads that line, and SKILL.md forbids it to drop
  --resume or add --deadline. Each gate is asked its own question (a zero
  compose floor refuses nothing; a reserve override below the floor names the
  verify refusal even where rounds are admitted), the start is named as the
  ledger entry or the plan's capture, minutes are pluralised.
- The round pricers read only this attempt's stamps (attemptStamps): a
  round-cap resume keeps the dead attempt's stamps, whose open span crossed
  the death gap and refused a rebuild on a wall the resume had just renewed.
  The one-stamp-per-round guard is per attempt for the same reason.
- cheapestRebuildAdmissionSeconds prices every round the stamps could make a
  `--round <k>` rebuild (each stamped round and the no-exclusion case) and
  takes the least; the compose-time waiver and the note both use it, so a FIX
  the gate would admit for some k is never waived.

The FIX waiver, explained:
- check-coverage reports the FIXes it withholds (`waived`), compose-review
  prints them as NOTE: lines beside the FIX lines and carries them on the
  composed verdict (`waivedFixes`; save-artifact defaults the field, run
  passes it through). The `[unverified]` relaunch FIX answers to the same
  verify floor. Notes are in minutes, like every other line on that channel.

The capture commands:
- capture-local's handler keeps plan-diff's contract: a usage error is one
  stderr line and exit 2, anything else exit 1 — never a crash banner for a
  malformed --deadline. The help text mentions --resume only on fetch-pr and
  says the grammar and the default rule still apply on a resume. A usage
  error echoes an over-long or non-string value bounded, in code points.
- fetch-pr's ledger note rides only where a plan wall would date from it.

Docs: DESIGN.md names the last admission each default allows; SKILL.md marks
the resume note as information and the withheld FIXes as NOTE: lines;
code-review.md carries the operator's remedies (a resume from a new session
first) and the withholding rule. Tests pin every arm above, isolated from the
ambient CI epoch; a mutation matrix over the new arms is listed in the PR.
2026-09-13 10:26:00 +00:00
00180c5360 feat: agent board — share work across independently started agents (#9402)
* feat(core): add ask and decision as board items

Stage 1 of docs/plans/2026-08-18-peer-session-collaboration.md. The board
holds task today; these are its two peers, under the same lock and
atomic-write discipline.

An ask is a question with terminal states — answered, declined, timeout — so
a sender always learns which and can wait, reroute, or escalate. That is what
a plain message cannot offer, and it is why the design has no general-purpose
message.

Timeout settles lazily. expiresAt is written at creation and any reader past
the deadline reports timeout; no sweeper walks the directory. A fetch-based
system has no daemon guaranteed to be running, so the only process that can be
relied on is the one currently asking. The deadline is re-checked under the
lock, since answering a lapsed ask would silently resurrect it.

A decision is something awaiting human authority — approval, acceptance,
adjudication unified, because each needs authority and no agent has more of it
than another. It has no expiry on purpose: silent expiry converts "nobody
looked" into "the system decided", which is the one authority nothing but a
human may hold.

Items land at ~/.qwen/boards/{board}/, the layout 2.8 settles, so they never
need the migration the existing split roots do.

board-lock.ts extracts the two-tier lock rather than adding a third copy of
it; folding tasks.ts and mailbox.ts onto it is a follow-up, kept out so this
change does not also rewrite those paths.

Refs #8724

* feat(cli): add the board command surface and fleet up

Stage 2 — the step where the requirement is actually met. With a command
line over the board, a Codex, a shell script, a scheduled job and a Qwen
session participate on identical terms, because running a command is the one
thing all of them can do.

qwen board show | watch | task | claim | done | ask | answer | decline
                | raise | resolve

Verbs are flat rather than nested under a noun — board claim t-3, not board
task claim t-3 — because the id prefix already carries the noun. Two levels
instead of three matters for a surface an agent types every turn.

board watch polls rather than watching the directory: fs.watch differs across
platforms and network filesystems, and a directory this small is not the
bottleneck. The panel leads with what needs a human, then what is blocked on
a peer, then work in flight — a panel that leads with "3 agents working" is a
process monitor, this one leads with what will not move until someone acts.

ask --wait is bounded (30s default) with distinct exit codes, since a foreign
agent running it is blocking its own turn.

fleet up writes no terminal code. tmux already gives panes, per-pane cwd,
keyboard switching, zoom, detach and a server that outlives the client, and
core already wraps its CLI. The command sequences those calls and adds the
one thing tmux cannot know about: a pane showing the board. Agents start via
the command passed at split time, so no keystroke can land before a shell is
ready. --with runs any other binary in its own pane, which is how a foreign
agent joins.

board-tasks.ts is deliberately not agents/team/tasks.ts: that module is Agent
Team's in-session list with dependency edges and an in-process change emitter
its scheduler subscribes to, keyed under a different storage root. Keeping
them apart lets this ship without rewriting that scheduler.

Refs #8724

* feat(core): make a session board-aware when QWEN_BOARD is set

Without this the feature does not work at all. fleet up starts each pane with
QWEN_BOARD in its environment, but a plain session ignored it — never
claiming a task, never looking for a question addressed to it, never raising
anything for the user. Panes of agents that cannot see each other, and a board
pane that stays empty.

The section is about when to reach for the board, not a command reference. A
model given verbs without boundaries uses ask for things it could read itself,
or narrates progress instead of moving a task. So it states that completing a
task is how you report, that an ask is for what only another participant can
answer, and that anything needing authority is a decision no agent resolves —
including itself.

It also pins the boundaries the design depends on and that prose would drift
away from: no general-purpose message, a named owner is a proposal rather than
an assignment, and your own output is not visible to anyone else.

Absent unless QWEN_BOARD is set, so the common single-agent prompt pays
nothing for it.

The CLI now re-exports the env var names from core instead of redeclaring
them. Two copies of a constant that must agree is the same failure mode this
whole design exists to remove.

Refs #8724

* docs(users): document the agent board

Written for someone who wants several agents working side by side, so it opens
with the command that produces that and a picture of the result rather than
with the model.

Three things it states plainly because they are the parts that surprise
people: the panel is ordered by what needs you first and hides anything
settled; --wait always settles, so a caller never hangs; and nothing is pushed
to any participant, which is exactly why a tool we did not write can take
part.

The limits section is honest about the two that matter — participation is
cooperative and cannot be enforced on an agent we did not write, and a named
owner is a proposal rather than an assignment.

* fix: repair the blockers a direction audit found

The audit found the command surface was unreachable and the layout broken,
which between them meant nothing in this PR could actually be exercised.

Blocker: qwen board and qwen fleet were registered but not in
parseArguments' exit list, so every invocation printed its output and then
booted a full interactive Qwen session on the same stdin. In a fleet pane
that is a TUI takeover; from a tool call it is a hung turn. It also made
run()'s exit codes meaningless, so the branching the ask contract promises
did not work.

Layout: tmux -l sizes the *new* pane, and the new pane was the board — so the
board took 70% and sat on the right, the opposite of both the intent and the
comment. Splitting N ways by hand also computed -l 100% for the last pane,
which tmux rejects, so the default --agents 2 failed on a half-built session.
Now the window's own pane is respawned as the board and main-vertical does
the arithmetic. Running inside tmux also used the configured session name
rather than the one the user is actually in, which fails whenever those
differ.

Ownership: claiming a completed task silently reopened and reassigned it, and
anyone could complete work someone else was mid-way through. Completion now
requires ownership; notes and hand-offs stay open to anyone, since those are
how a stuck board gets unstuck and neither destroys work.

Identity: the participant fallback included the pid, so every invocation was
a different participant — claim and done on one task came from two identities
and an ask could never be addressed back. Stable per-user now.

Also: --ttl on ask, since a recipient that only looks between long turns
could not answer within the fixed 15 minutes; --wait reads one item instead
of listing the board every 500ms; and the asks header no longer claims the
timeout is persisted, because it never was and persisting it would mean a
read path that writes.

* feat(cli): hand the protocol to agents we did not write

The audit was right that --with was hollow: it set QWEN_BOARD in a foreign
pane, but a Codex never reads that variable and nothing we control can inject
into its prompt. The env var alone made heterogeneous participation look
supported while leaving the operator to reconstruct the protocol by hand —
the exact thing an access layer exists to avoid.

qwen board protocol prints the same instructions a Qwen session gets, filled
in with the real board and participant name, in a form meant to be pasted.
--with panes now run it before exec'ing the command, so the text is already
in the pane the agent lives in.

This does not make participation enforceable, and the docs say so. It makes
it possible without a human reading our source.

Also adds show --mine, which narrows tasks and asks to one participant while
deliberately keeping decisions: those are the human's, and a view that hides
them cannot tell the user what is blocking the board.

* fix(cli): repair fleet layout targeting and the type error the audit caught

The second audit found this branch did not typecheck: fleet.ts passed
tmuxListPanes' result — already TmuxPaneInfo[] — back into
parseTmuxListPanes. Same failure mode as the round before: a path written
against an imagined API and never exercised.

Worse was what it did when it ran. Inside tmux, the code asked tmux for "the
current window" after creating one. For a non-attached command client that
resolves from the inherited $TMUX_PANE — the window that invoked us — so
fleet up respawned its own pane and killed itself mid-build. tmuxNewWindow
now returns the new window's id via -P -F, which is the only thing that can
name the window we just made.

Also from the audit: the layout ran out of rows past three or four panes
because every split halved the same pane, so it now rebalances after each;
re-running outside tmux reused an existing window and destroyed the earlier
board, so it always makes a fresh one; tmux failures after verifyTmux escaped
as a raw stack trace through the yargs handler, now contained to one line;
and show --mine filtered only the human render, making the flag a no-op for
--json, which is the branch a foreign agent uses.

The --with pane no longer prints the protocol before exec'ing: agent TUIs
enter the alternate screen and discard it, so the banner was never readable.
The operator is told once, after the layout is up, to paste qwen board
protocol in. The docs said the environment variables were enough for a
foreign tool to join, which was never true; they now say what actually works,
and record --ttl, --mine and the running-session limitation.

* feat(cli): let an already-running session join a board with /board

The last requirement still unmet. Board awareness came only from QWEN_BOARD,
which fleet up sets in each pane's environment — and an environment is fixed
at launch. A session that was already deep in a problem when coordination
started could never participate, which is precisely the case this design
exists for.

resolveBoardPromptContext now checks a runtime context before the
environment, and /board sets it and refreshes the system instruction, so the
board section is present from the next turn. getMainSessionSystemInstruction
rebuilds the base prompt on every refresh, so no cache had to be invalidated.

/board          reports where you are
/board <name> [as <who>]
/board off

The point is that the joining session keeps its context. Briefing a fresh
teammate is what spawning already does; bringing in an agent that has been
working for an hour is what this is for.

Docs record it, and the limits section now says what is actually true — a
running session can join, but nothing is pushed to it, so it sees an item the
next time it looks.

* feat(cli): add board prune, and stop overstating decision authority

prune was specified in the design and referenced by a comment in asks.ts for
a command that did not exist, so settled items accumulated forever. It is
manual rather than scheduled: deleting a record another participant may be
mid-read on is a concurrency problem worth not having, and a fetch-based
system has no daemon that could be trusted to sweep. It takes the item lock
so a concurrent settle cannot be lost between the read and the unlink.

decisions.ts claimed no agent-visible tool routes to resolveDecision. That was
false — qwen board resolve is on the same CLI the prompt teaches agents to
use, so anything with a shell could settle its own decision. Enforcing it
needs a surface the agent panes do not carry; until then the code, and now the
user docs, say "by agreement" rather than pretending it is an invariant.

Two smaller repairs from the audit: the lock map grew one mutex per touched
path for the process lifetime, which a long-lived board watch would accumulate
indefinitely, and a file removed between taking the lock and reading it
surfaced as a raw ENOENT instead of the not-found the onMissing contract
promises — reachable now that prune exists.

Also names the board pane, which showed the hostname before.

* feat: register participants so peers can be addressed by name

Until now the only participants visible were those who had already owned a
task or been party to an ask. An agent that joined a minute ago could not be
addressed, because nothing knew its name — and `board ask <name>` had no way
to learn one, so callers were left guessing.

participants/{name}.json records who is on a board. It does not duplicate the
machine-wide session registry: that one is keyed by pid and answers what is
alive, this one is keyed by declared name and answers who is here. Liveness is
read from the former, so a record never has to be heartbeated — a crashed
agent stops being listed because its pid is gone.

A name held by a live process is suffixed rather than refused: a caller made
to retry with a different name will either pick badly or give up. A name whose
holder is gone is reclaimed, or a crash loop would exhaust every reasonable
one.

Panes join before the agent starts, and /board joins on the way in, rather
than instructing the model to run it. A participant nobody registered cannot
be addressed, which is too load-bearing to leave to whether an agent follows
its prompt.

kind — interactive | daemon | spawned | foreign — is the field that separates
an agent a leader started, which exists to do what it is given, from an
independent one with its own work, for which an assignment is a proposal.

* feat(cli): poll the board so an idle session stays responsive

A session mid-turn reads the board at its turn boundary, but one sitting idle
at a prompt has no boundary — so an ask addressed to it would sit untouched
until its TTL lapsed. That gap is the strongest argument for push, and it does
not need push to close.

useBoardPending polls every five seconds: below what a person notices, and one
readdir plus a few small reads, the same order as sessions ps. The session
still chose the moment and nothing can arrive unbidden, so none of the
machinery a delivery path would need comes with it. Decisions lead the
indicator and carry the mark — an ask blocks one peer, a decision blocks
everyone until a person acts.

Deliberately not wired into Footer.tsx. That is a 320-line ternary chain whose
rendered output I cannot see from here, and blind edits to unverifiable code
produced both of this branch's earlier blockers. The hook and its formatter
are tested; wiring is one branch in leftBottomContent and wants a visual
check.

An unreadable board — removed, permissions changed — drops the indicator
rather than interrupting the session.

* feat(cli): show what the board is waiting on in the footer

Completes the idle path. The hook polled but nothing rendered it, so a session
sitting at a prompt still had no reason to look.

It goes in rightItems, next to sandbox and safe-mode, rather than as a branch
in leftBottomContent: the indicator is additive and should not displace the
shortcut hint. Warning-coloured because everything it counts is something
waiting on someone, and decisions lead — an ask blocks one peer, a decision
blocks everyone until a person acts.

The previous commit left this out for fear of editing a component whose render
could not be seen from here. That was the right caution but the wrong
conclusion: Footer.test.tsx renders with ink-testing-library, so the wiring is
asserted rather than eyeballed. Three cases — no board, quiet board, counts
present — and the existing 34 still pass.

* refactor(core): fold the three copies of the file lock onto one factory

tasks.ts, mailbox.ts and board-lock.ts each carried the same two-tier
discipline — an in-process mutex per path so local writers do not stampede the
OS lock, wrapping a proper-lockfile cross-process lock. mailbox.ts's version
was already commented as mirroring tasks.ts's.

They are not identically tuned, and flattening that would have been the wrong
kind of consolidation: mailbox retries 10 where the task board retries 30, and
logs a compromised lock at debug rather than warn. An inbox has one writer per
message; the task board has up to MAX_TEAMMATES contending for the same
first-pending file, which is what the deeper budget and the jittered backoff
are for. So createItemLock takes the budget and the handler, and each caller
keeps its own.

onMissing is now optional and rethrows when absent, which is what mailbox
expects — its callers treat a vanished inbox as a real error, while the task
board and the board items treat it as not-found.

disposeInboxLocks survives as a thin wrapper over the factory's dispose: the
mutex map moved inside, so eviction had to move with it.

Verified: 301 tests across src/agents/team pass, including tasks, mailbox and
the 53-case coordination harness.

* feat: task dependencies and the deadlock the design promised

Closes the last open item. Two things the design sold and the code did not
have.

BoardTaskRecord gains blocks/blockedBy, written on both sides by
blockBoardTask. Both halves are stored rather than derived because a reader
holds one task at a time — answering "what is this waiting on" from a one-sided
edge would mean scanning every other task. A blocker that no longer exists
counts as satisfied, so a pruned task cannot wedge the board forever.

findDeadlocks is the part 3.3 called the thing no single session can see about
itself: from inside, A is simply waiting, and so is B. On the board those are
two open asks pointing at each other, so the cycle is data rather than an
inference from two stalled sessions. It renders above the asks it is made of,
because a pair that cannot resolve itself needs a person and nothing else
surfaces it.

Only the two-party case. Longer cycles exist but are rare enough that
reporting them costs more confusion than it saves, and a pair is what someone
can act on immediately.

Verified: 305 tests across src/agents/team, 12 in the board CLI, core
typechecks clean.

* fix(core): type sharp by its module shape so the package builds

packages/core does not compile against its own declared dependency range.
image-view.ts imports SharpConstructor from sharp, and no such export exists
in ^0.34.5 — resolved here to exactly 0.34.5, whose types are export = sharp,
a function merged with a namespace and describing that shape under no name.

typeof import('sharp') is what the file actually needs: callable for
sharp(bytes, …) and carrying the namespace members the resize path reads
(sharp.kernel.lanczos3).

Found while trying to verify the board work end to end from build output — the
build failed before reaching it, and would fail the same way for anyone
installing to the manifest. Carried here rather than opened separately because
it is a prerequisite for building this PR at all. packages/cli still fails on
nine errors in acp-bridge and channels/{dingtalk,feishu}, which are untouched
by this branch.

Verified: packages/core builds clean, image-view tests pass.

* fix: stabilize agent board fleet startup

* chore(cli): use standard command output streams

* feat(cli): make decision authority structural rather than a request

The last thing in this design that was asserted but not held. decision exists
because approval, acceptance and adjudication need authority no agent has —
and that was a sentence in the prompt, on a command line agents use for
everything else. Anything with a shell could approve its own request.

qwen board resolve now refuses from a non-interactive shell, which is what an
agent's tool call is: its stdout is captured, a person's terminal is not.

A barrier, not a proof. --force bypasses it for scripting, and an agent that
allocated a pty would pass. Both are acceptable, because the threat is a model
taking the path of least resistance rather than one working to defeat a check.
What changes is that settling your own request is no longer the easiest thing
available.

The check lives in the CLI rather than resolveDecision because only the caller
can see whether a person is there. decisions.ts, the board prompt and the user
docs now all say the same thing, where before the code claimed an invariant it
did not have.

Verified: 13 board CLI tests, 25 across board-prompt and board-items.

* fix(core): restore a callable sharp type in image-view

`(typeof import('sharp'))['default']` aliases the module-namespace
shape, which is not callable, so the `sharp(bytes, ...)` call sites
fail to build. sharp is `export = sharp` (a function merged with a
namespace), so `typeof import('sharp')` is already the callable
function+namespace type.

* fix(board): guard os.userInfo() in the participant-name fallback

os.userInfo() throws SystemError when the effective uid has no passwd
entry (containers, root). Fall back to USER/LOGNAME before a generic
name so the zero-config default still resolves.

* fix(board): never prune on an unparseable settle timestamp

A truthy non-numeric timestamp made  NaN, and 
is false, so the age gate skipped its  and unlinked the file. Resolve
the timestamp to a finite millisecond value first and skip anything that does
not parse.

* fix(board): sanitize record-controlled text before terminal rendering

renderBoard interpolated task/ask/decision/participant fields raw into
the terminal, so a participant-written ANSI/C0 sequence would be
interpreted by the terminal. Route every record-controlled field through
sanitizeTerminalText.

* fix(board): parse the block command and reject NaN numeric options

- "block <id> --on <blocker>" parsed --on and <blocker> as demanded
  positionals, so the block command could never run; --on is an option
  now.
- yargs type:number delivers NaN for non-numeric input; route
  older-than/interval/timeout/ttl through finiteNumber so a poisoned
  number fails with one line instead of deleting everything (prune) or
  never firing (wait).

* fix(board): reject a NaN --agents in fleet up

yargs type:number yields NaN for non-numeric input, and the loop
condition i <= Math.max(0, NaN) is always false, silently spawning
zero agents. Reject non-finite --agents with one clear error.

* fix(board): keep the collision-suffixed name inside SAFE_NAME

A 64-char base plus a numeric suffix exceeded the 64-char limit, so the
claimed name produced a record no reader could address. Truncate the base
to leave room for the suffix.

* fix(board): move the mutex-eviction check outside runExclusive

The eviction ran inside the runExclusive callback, where isLocked() is
always true because the caller itself holds the mutex, so the delete was
unreachable and the map grew without bound. Run it in a .finally() after
runExclusive releases the mutex.

* fix(board): prune asks that settled as timeout

* fix(board): age timeout asks by their expiresAt in prune

* fix(board): carry --board/--as in the protocol command examples

The emitted instruction text gave commands without --board/--as, so a
recipient running in a different directory or as a different user would
target the wrong board or identity. Inject the flags into every command.

* fix(core): restore a callable sharp type in image-view

sharp 0.35 ships dual type entries: the ESM `.d.mts` declares
`export default sharp`, while the CJS `.d.cts` uses `export = sharp`.
packages/core is ESM, so `typeof import('sharp')` resolves to the
non-callable module namespace; index `['default']` to reach the callable
function+namespace type that the resize path (sharp.kernel) needs.

* fix(board): resolve the tsc errors blocking the board build

- pruneCollection redeclared `raw` in one block; rename the settle
  timestamp to `timestamp` so the file contents read keeps `raw`.
- access USER/LOGNAME through the ProcessEnv index signature
  (noPropertyAccessFromIndexSignature).
- supply the missing participantCount on the deadlock renderBoard call.

* fix(board): satisfy ESLint for the board files

- board.ts: route the non-interactive resolve refusal through
  process.stderr.write instead of console.error (no-console)
- render.ts: merge the two @qwen-code/qwen-code-core type imports
  (import/no-duplicates)
- rename boardCommand.ts / useBoardPending.ts to kebab-case
  (check-file/filename-naming-convention) and update their imports

* fix(board): move board exports off the core barrel to keep iconv-lite out of the ACP closure

The board/fleet `export *` lines on the core barrel let the ACP agent
runtime (which imports the barrel) pull the board's dependency chain —
sync-file-encoding → iconvHelper → iconv-lite encoding tables — into its
static startup closure, tripping the serve fast-path bundle check. Re-export
the board surface from a dedicated `@qwen-code/qwen-code-core/board`
subpath and point the CLI importers at it; the barrel no longer carries the
board, so the ACP closure stays clean.

* fix(board): restore CI coverage

* fix(i18n): register board command description

* fix(board): narrow the collaboration MVP

* fix(board): harden persisted record boundaries

* test(board): satisfy terminal sanitizer lint

* fix(board): close round-4 findings — keep emit multiline, guard renders, fix dead fixtures

* docs: include agent board collaboration design

* refactor(board): narrow the collaboration surface

* fix(board): keep piped --json intact and validate --timeout before writing

`parseArguments` calls `process.exit` as soon as a board handler resolves.
`emit` wrote to stdout without waiting for the flush, so a `--json` snapshot
larger than the pipe buffer was truncated mid-record while the command still
exited 0. Waiting for the write callback also means a reader that closes early
has to be handled, since a closed pipe is reported both to the callback and as
a stream error event.

`ask --wait` validated `--timeout` only after `createAsk` had already
persisted the ask, so a rejected timeout left an orphaned open ask on the
board. The local wait is now validated before anything is written, matching
how `--ttl` already behaved.

* fix(board): report pruned items by id rather than filename

`prune --json` listed `a-<uuid>.json` / `t-<uuid>.json` while every other
machine-readable board surface identifies items by bare id, so a script
joining prune output against ids from `show --json` matched nothing.

* docs(board): clarify collaboration delivery sequence

* fix(core): accept settled timeout state in board ask records

parseAsk rejected state 'timeout' although AskState declares it and
settleAsk produces it, so a timeout record written to disk by a foreign
runtime sharing the board was silently dropped from listings and treated
as missing by getAsk/answerAsk/declineAsk. Accept it with the shape
settleAsk emits (settledAt present, no answer or reason payload).

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

* test(core): cover ask TTL transition, self-ask guard, and task pruning

Adds a dedicated asks suite: the expired-open auto-transition to timeout
that the CLI exit code 3 relies on, the createAsk from===to refusal,
foreign timeout records (valid and malformed), and parse-level rejections
exercised through getAsk/listAsks. Also pins pruneBoardTasks, which only
prunes completed tasks keyed on updatedAt — a path the pruneAsks tests do
not share.

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

* fix(core): bound board read fan-out and fold board name case

Two storage-layer fixes from the second-pass review, plus the two tests
that pin guarantees the suite did not pin, and one interop doc line.

listBoardTasks/listAsks opened one fd per record through a single
Promise.all, so peak fds grew linearly with the entry count and a board
larger than a low hard RLIMIT_NOFILE (docker --ulimit nofile=, systemd
LimitNOFILE=, CI runners) failed the whole listing with EMFILE and no
partial results. Read in batches of 32 instead; the trailing createdAt
sort keeps the result order unchanged.

Board names reach the filesystem verbatim, and APFS/NTFS fold case while
ext4 does not, so Sprint and sprint were one board on macOS and two on
Linux. Fold case where the directory name is derived, so one name means
one board on every filesystem.

board-items.test.ts: the prune re-check test mutated the record after a
single setImmediate, which lands before prune's readdir returns, so a
read-before-lock implementation still passed. The test now holds the
cross-process lock itself and waits until prune is provably blocked on
it before reopening the ask.

board-tasks.test.ts: adds the contended-claim test (deleting the
"already claimed by" guard turns 1 winner into 8) and a listing test
over more records than one read batch.

agent-board.md: an expired ask stays state "open" with settledAt null on
disk because timeout is derived on read, so a foreign reader must apply
now >= expiresAt itself.

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

* fix(ci): cover the ./board core export in the capture loader

Merging main brought scripts/tests/text-capture-core-loader-sync.test.js, which requires the skill-review-harness loader's inline named map to carry one entry per named key of core's exports map. This PR adds ./board (packages/core/src/board.ts, dist/src/board.js), so the map needs ['board', 'board.ts'] -- a missing entry whose specifier has no mirrored src file falls through to the exports map and the capture dies with ERR_MODULE_NOT_FOUND on a fresh clone.

Also format packages/core/src/agents/team/asks.test.ts. Step 8 of Lint & Static (Check lint gate freshness) had been failing in ~24s on every prior run of this branch, which short-circuited step 26 (Run Prettier); once the gate could run, prettier flagged this file's multi-line import, which fits on one line. Formatting only, no assertion touched.

Verified: vitest run --config ./scripts/tests/vitest.config.ts on text-capture-core-loader-sync.test.js and integration-tsconfig-core-paths-sync.test.js = 2 files / 4 tests passed; prettier --check clean on both changed files.

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

Patrol-Run: qwen-pr-closeout/jmtzb8sj51m

---------

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-09-13 10:21:45 +00:00
易良andQwen-Coder 9d69c91a64 fix(web-shell): pin es2021 floor across both build configs and harden terminal tests (#11758)
- Resolve the build-config probe through web-shell's own Vite 5 instead of
  the root-hoisted Vite 7, parameterize it over vite.config.ts and
  vite.lib.config.ts, and pin build.target itself (R1-1, R1-2).
- Share WEB_SHELL_BUILD_TARGET between the app and lib configs so the
  published lib bundle gets the same es2021 floor (R1-2).
- Pin the localized protocol-mismatch notice and the releaseRequested flag
  in the mismatch test (R1-3, R1-4).
- Drop the duplicated node-pty pin count owned by conpty-host.test.ts (R1-5).

Co-authored-by: Qwen-Coder <[email protected]>
2026-09-13 09:50:26 +00:00
Dragon e6647b3a3b fix(cua): bound observations and reduce model round trips (#11735)
* fix(cua): retain bounded observation baselines and cap text

* perf(cua): reduce avoidable computer-use round trips

* perf(node-repl): avoid repeated runtime rules in tool discovery

* fix(cua): recognize attached sheets in exact-target validation

* fix(cua): expose known refusal categories in app errors

* perf(cua): Combine macOS bootstrap with initial app observation

* perf(cua): Prefer exact selection for field replacement

* perf(cua): Lead field edits with verified text selection

* chore(cua): reconcile local optimizations with updated App routing
2026-09-13 09:12:48 +00:00
顾盼 5f57c78b06 feat(cua): add app-bound actions and compact native observations (#11705)
* feat(cua): add app-bound actions and compact native observations

* fix(cua): complete native app input and AX alignment

* feat(cua): add macOS text operations and platform skills

* fix(cua): preserve app observation context

* fix(cua): reduce app action recovery failures

* fix(cua): align macOS app targeting with native service
2026-09-13 09:12:48 +00:00
tanzhenxin 2eaed75010 fix(cua): address tool failures and efficiency in OSWorld runs (#11683)
* fix(cua): address tool failures and efficiency in OSWorld runs

* docs(cua): consolidate efficiency bugfix design notes
2026-09-13 09:12:47 +00:00
易良andQwen-Coder f1d5a64bbf fix(core): strip AppImage PYTHONHOME/PYTHONPATH from stdio MCP children (#11763)
* fix(core): strip AppImage PYTHONHOME/PYTHONPATH from stdio MCP children

The Desktop AppImage exports its bundled Python's PYTHONHOME/PYTHONPATH
globally; a stdio MCP server's own interpreter then resolves its standard
library against the AppImage mount and crashes at startup (#11718). Strip
both from the inherited environment when spawning stdio MCP servers under
the desktop shell only: the CLI leaves a user's own Python setup untouched,
and an explicit per-server env entry still wins.

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

* fix(core): match the desktop marker read used elsewhere ('1')

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-09-13 09:00:13 +00:00
Shaojin Wenandprobe 79dabcb832 fix(web-shell): keep mid-turn messages the daemon rejects at idle (#11289)
A mid-turn message the daemon refuses with `session_idle` is not a failure:
the session can take an ordinary prompt instead. The web shell fell back to
that route but then had no way to tell what the daemon did with the message,
so it surfaced a rejection toast for a message that had run, or echoed it
twice, or dropped it in silence — and a queue row could be left that the user
could neither see nor remove.

The fallback now confirms the resubmission against a pending-prompt snapshot
instead of trusting the activity mirror, which another client's queued prompt
can lag, and a row the daemon refused at idle carries that provenance through
a hold and the drain so a later release confirms the same way.

Echoes are sourced from the payload the client actually holds — a row bound by
the daemon's id, a stash keyed by it, or the started event's own rendering —
and every route that echoes leaves a record that outlives the settle, so no
submit body appends the same message twice. A start that arrives while a
removal is in flight parks, and every removal-failure arm replays it, so a
message the daemon really ran is not lost because the user's cancel lost the
race; it replays only this client's own messages, and never as the daemon's
`[image]` placeholder. Attribution of a suppressed start to a row requires an
event this client owns, because a rendering is not an identity.

A row the drain has stamped but not yet released cannot own a snapshot entry
or a started event, so it is out of every match count: it can no longer be
spliced or bound to a prompt it is not, which read as a cancellation and
bailed without posting, reporting or restoring anything. A cancellation that
could not be confirmed is recorded and applied by the next snapshot that can
prove something, and an owner change issues those removals rather than wiping
them.

Also documents the mid-turn resend rule after a refused close: waiting for the
promotion is the default and is loss-free, and forcing the drain means
releasing every still-queued id before re-posting any, because no DELETE
verdict on its own establishes delivery.

Co-authored-by: probe <probe@local>
2026-09-13 08:20:25 +00:00
b3975b7568 fix(cli): prevent dialog clipping in short terminals (#9040)
* fix(cli): prevent dialog clipping in short terminals

* fix(cli): address dialog height review feedback

* test(cli): pin dialog height budget branches and document locked-skill collapse

* fix(cli): make dialog height budgets width-aware

Cap MultiSelect labels to the render width and truncate counted single-line texts so the height budget holds at any terminal width; give the interactive unlocked list first claim on flexible rows and let the locked block collapse to (or drop) its hint instead of overflowing.

* fix(cli): harden dialog height budgets

* fix(cli): shed dialog chrome below the height-budget floor

Mirrors the /statusline compact path in /skills: budgets at or below the
fixed frame drop border, paddingY, and footer so the always-rendered
list row fits. Truncates every remaining counted text (translated
titles, Type to search, Preview) at any width, folds \v/\f in status
line fields, and notes hidden locked skills in the subtitle when no
locked row fits (round-5 review).

* test(cli): pin round-5 budget fixes

Covers the compact-frame budgets (11/10/8), the locked-only query at a
frame-floor budget, the ca-locale 26-column title, the subtitle locked
count at residual === 1, \v/\f folding, and diverges the two width
sources in the DialogManager wiring test (round-5 review).

* fix(cli): translate the locked-skills subtitle badge

* fix(cli): shed skills dialog chrome to the bare list at tiny budgets

Round-7 R7-1: the /skills compact frame still floored at 6 rows
(title + subtitle + search + list margin + the forced list row), so
height budgets <= 5 overflowed and overflow="hidden" clipped the
interactive list itself. Add a bare tier that sheds the remaining
chrome at budgets <= 5 so only the list area renders, mirroring the
/statusline compact path. The hidden search row also bypasses and
suppresses query mutation so a retained query cannot filter invisibly.

* fix(cli): simplify short skills dialog handling

* test(cli): pin review round-9 suggestions for dialog height fix

- assert absence of (+N locked) badge in unconstrained all-locked dialog
- cover \r in status-line control-character collapse cases
- use feminine participles in fr/ca locked-count badge translations

* fix(cli): address dialog height review feedback

* fix(cli): preserve dialog search and label behavior

* test(cli): restore skills dialog config mock

* fix(cli): preserve locked skill search within height budgets

* docs: record PR 9040 review and verification follow-ups

---------

Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-09-13 07:58:04 +00:00
qqqysandqqqys 271c968095 fix(dingtalk): preserve callback text (#11757)
Co-authored-by: qqqys <[email protected]>
2026-09-13 07:56:41 +00:00
qqqysandqqqys ee1ebcc167 fix(dingtalk): handle replied rich-text media (#11759)
Co-authored-by: qqqys <[email protected]>
2026-09-13 07:13:48 +00:00
35a9c5c62d perf(web-shell): load metadata on demand and reuse capability checks (#11644)
* perf(web-shell): load metadata on demand and reuse capability checks

* fix(web-shell): address demand-loading review regressions

* fix(web-shell): address demand-loading review suggestions

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

* fix(web-shell): pass explicit initial value to git poll target ref

React 19 typings require useRef to receive an initial value; the
zero-argument call failed tsc -p tsconfig.lib.json (TS2554) and
rejected the previous round's build.

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

* fix(web-shell): restore slash input after pre-admission failure

---------

Co-authored-by: 钉萁 <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
2026-09-13 06:18:28 +00:00
e6e075996a fix(cli): break Ink useBoxMetrics commit-phase setState loop (React #185) (#11565)
* fix(cli): break Ink useBoxMetrics commit-phase setState loop (React #185)

* fix(cli): scope the ink useBoxMetrics loop guard per instance

The guard added to the vendored Ink patch kept its 32-entry budget in
module scope, so every useBoxMetrics instance shared one budget, and the
budget counted invocations rather than oscillation cycles. With 40 boxes
mounting in a single commit, instances past the 32nd invocation never
measured: hasMeasured stayed false for good, because no later commit
re-fires their layout listener. Hold the budget in a ref so each instance
refills on its own.

Cancel the pending budget-refill timer on unmount. Vitest fakes
setImmediate by default and vi.useRealTimers() discards a pending faked
immediate, so the stored handle stayed non-null after the timer was gone
and the counter never refilled.

Drop the functional same-value bail for setHasMeasured. The useState setter
already bails for an identical primitive, and the functional form cannot be
eagerly bailed - React has to schedule the render to evaluate the updater.
Reverted to upstream's setHasMeasured(Boolean(ref.current)).

Regenerate patches/ink+7.0.3.patch with patch-package instead of
hand-appending the hunk, so it lands in path order with an index line and
function-context headers. The regeneration also reformats the pre-existing
hunks (index hashes, hunk-context labels, offsets); that churn is cosmetic,
and the only content change outside the new hunk is one
"\ No newline at end of file" marker.

Adds packages/cli/src/ui/use-box-metrics-loop-guard.test.tsx, which renders
the real hook and pins three behaviours: an oscillating box settles without
React #185, a later resize still re-measures once an oscillation tripped the
guard, and 40 instances all measure within one commit.

* fix(cli): refill the ink useBoxMetrics budget from elapsed time

The measurement budget added to the vendored ink hook refilled itself from
a setImmediate callback. A test that installs fake timers only runs that
callback when it advances the clock, and the InputPrompt suite flushes
renders with microtask yields inside act(), so the refill stayed pending
and React's act queue never drained with it. Ten tests in that file each
burned their full 15s timeout; on CI, where the suite runs with --retry=2,
that single file outlived the 60 minute job limit and the lane was
cancelled without printing a summary.

Refilling from elapsed wall-clock time keeps the same per-instance budget
and the same protection against the commit -> layout listener -> setState
loop, and leaves no handle for a test to drain, so the unmount cleanup is
no longer needed either.

---------

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-09-13 06:05:26 +00:00
易良andQwen-Coder bc7a186cda fix(web-shell): finish terminal replay follow-ups (#11748)
Co-authored-by: Qwen-Coder <[email protected]>
2026-09-13 05:09:29 +00:00
ddd6eea9dd test(core): account for the extra Windows fstat in the tail-growth session mocks (#11750)
The no-follow open fallback used when O_NOFOLLOW is unavailable performs an lstat/open/fstat identity check while opening the file, adding one fstat before the first tail read. Two tests inject a stale size on the first observed fstat to simulate growth between the stat and the read; on Windows the injection now lands on the identity fstat, so the first tail read sees the final file and one case fails deterministically in the nightly Windows lane.

Skip the open-time identity fstat (one call, only where O_NOFOLLOW is missing) so the stale size reaches the first real tail read on every platform. POSIX behavior is unchanged, and the sibling re-read case now actually exercises its intended stale-tail race on Windows.

Regression from #10007, which landed after the #10064 Windows lane sweep.

Co-authored-by: F415643 <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-09-13 04:03:54 +00:00
Jeremy Schoemaker fa20933437 fix(dashscope): send request metadata only for qwen-family models (#11606)
* fix(dashscope): send request metadata only for qwen-family models

Fixes #11590.

DashScope's OpenAI-compatible endpoint is an aggregating gateway. Qwen Code
injects a top-level `metadata` object (sessionId / promptId / channel) into every
request sent there, but for a non-qwen model such as `ZHIPU/GLM-5.3-Flash` the
gateway forwards the body to that vendor's own backend, where `metadata` is typed
as a string. Deserialization fails and the request comes back as a flat
"400 API 调用参数有误", which makes the model completely unusable through Qwen
Code. The reporter captured two request bodies differing in exactly one
top-level field: the one carrying `metadata` 400s, the one without it returns
`finish_reason: stop`.

`metadata` is a platform-private tracing field that only means anything to
DashScope's own inference path, so it is now gated on the wire model. This is the
same gate, for the same stated reason, that `buildQwenEffortConfig` already
applies to the qwen-only thinking knobs a few methods above: "so the
qwen-specific fields never leak to a non-qwen model sharing the DashScope
endpoint."

Scoped deliberately:

- `buildMetadata` keeps its signature and behavior; the gate lives in a new
  private `buildRequestMetadata` used at both spread sites, so the existing
  public method and its tests are untouched.
- Both the vision and non-vision paths are gated. In practice every model in
  VISION_MODEL_PREFIX_PATTERNS is qwen-family and `coder-model` is matched by
  isQwenFamilyWireModel, so the vision path is unchanged today; gating both keeps
  them from drifting.
- This does not walk back #9103. That PR widened which *origins* count as
  DashScope-compatible (`*.alicloudapi.com`), which is orthogonal to which models
  get the field. A qwen model behind such a gateway still ships metadata, and
  there is a test for exactly that.

7 tests added. The three "omits metadata for the non-qwen model" cases fail
against unpatched sources; the qwen-family and alicloudapi cases guard against
regressing the other direction. dashscope.test.ts: 176 passed. tsc, eslint and
prettier clean on both files.

* feat(dashscope): add enableRequestMetadata to override the qwen-family gate

Addresses the review's first concern on #11606: the gate keys on the model, but
the client cannot tell a request DashScope forwards to a third-party backend
from one DashScope serves itself. A non-qwen model on a first-party host under
QWEN_OAUTH loses sessionId/promptId correlation too, and that request does not
400, so nothing is bought by dropping the field there.
packages/core/src/core/openaiContentGenerator/pipeline.test.ts exercises exactly
that shape (glm-5.2 under QWEN_OAUTH).

`enableRequestMetadata` is tri-state and mirrors the existing
`enableCacheControl` off-switch, read off the same config object by the same
accessor: undefined keeps the auto behavior (qwen-family only, which fixes
#11590), `true` restores the field for first-party non-qwen sessions whose
tracing matters, `false` suppresses it everywhere.

Kept as its own commit so it can be dropped if the preferred answer is the model
gate alone, or if the field should instead be stripped gateway-side.

3 tests added. dashscope.test.ts: 179 passed. tsc, eslint, prettier clean.

* fix(dashscope): wire enableRequestMetadata through model configuration

The previous commit added enableRequestMetadata and the provider reads it off the
resolved ContentGeneratorConfig, but nothing populated it. resolveGenerationConfig()
copies only the fields in MODEL_GENERATION_CONFIG_FIELDS, and that list did not
include it, so a settings or modelProvider generationConfig entry was discarded
before it ever reached the provider. The option was readable and unsettable: the
escape hatch documented on the PR could not actually be used, and the tests did not
catch it because they injected the config getter directly and so never exercised the
delivery path.

Registered in both places the field has to appear: MODEL_GENERATION_CONFIG_FIELDS in
models/constants.ts, which is what the resolver iterates, and the ModelGenerationConfig
Pick in models/types.ts, which tsc flagged as soon as the first was added. This is the
same pair enableCacheControl occupies, which is the switch this option was modelled on.

4 tests added in modelConfigResolver.test.ts going through resolveModelConfig rather
than a stubbed getter: settings value carried through with the right source, an explicit
false carried through so the option can suppress as well as restore, modelProvider
winning over settings, and undefined staying undefined so the model-family default
holds. Three of the four fail without the constants change.

models + dashscope suites: 431 passed. tsc, eslint and prettier clean.

Reported by @doudouOUC on #11606.

* docs(settings): document enableRequestMetadata in model.generationConfig

The delta review on #11606 noted the option was settable but undocumented:
docs/users/configuration/settings.md enumerates the supported
model.generationConfig keys and did not list it, which is where a user would
look now that the key actually reaches the provider.

The entry also answers the second suggestion from that review rather than
changing behavior for it. An explicit false does not survive a model switch,
because resolveGenerationConfig re-derives the field on each auth/model
resolution. That is pre-existing pipeline behavior shared with
enableCacheControl and samplingParams, not something this PR introduces, so the
docs say the value is re-derived instead of giving this one field a bespoke
re-apply that its siblings do not have.

git diff -w is 2 insertions and 2 deletions; the other 25 changed lines are
prettier repadding the table, since the model.generationConfig row was already
the longest in it at 1369 characters and any addition widens the column.

* docs(settings): point provider models at the key that actually applies 📍

Addresses the two documentation points from the maintainer verification on

Where the override lives. `enableRequestMetadata` was documented only under
`model.generationConfig`, but for a model declared in `modelProviders` that
copy is ignored, which is #11590's own configuration shape. Now says to define
it in `modelProviders[].generationConfig.enableRequestMetadata`, matching the
wording `customHeaders` and `extra_body` already carry on the same line.

Removed "re-derived on each auth/model resolution, so it is reapplied when you
switch models". On the `openai` auth path `/model <id>` requires
`modelProviders`, and with `modelProviders` present the
`model.generationConfig` copy is ignored, so the sentence promised something
unreachable on the path most users are on.

Added a note that an explicit true/false is read from the session's ambient
configuration, so a side query issued for another model may not observe a
per-model override. That is the session-scope caveat rather than a behaviour
change; reading it off the provider's own contentGeneratorConfig is worth
doing separately, and it only affects the explicit switch, not the
model-family gate that is the actual fix.

`git diff -w` is 2 lines. The rest is prettier realigning the table columns
after the cell grew. prettier --check is clean and was clean before.

* test(dashscope): pin the metadata gate on the vision call site 🔍

Closes the coverage note from the maintainer verification on #11606: both
call sites were gated at runtime, but only the non-vision one had a test.

buildRequest has a second, separate return for vision models. A regression
there would leave `metadata` on the wire for a non-qwen vision model while the
non-vision path stayed correct, which is the exact failure the gate exists to
prevent, and nothing would have gone red.

Two tests, mirroring the existing pair: metadata ships on the vision path for a
qwen-family vision model, and is suppressed there under
enableRequestMetadata: false. Both assert vl_high_resolution_images is true so
a future change that routes away from the vision branch cannot make them pass
for the wrong reason.

Verified by mutation. Swapping buildRequestMetadata for buildMetadata at the
vision call site only, leaving the non-vision site intact, turns the
suppression test red and leaves the other green, which is the correct split.

dashscope.test.ts plus packages/core/src/models: 433 passed. prettier and
eslint clean.

* fix(config): carry enableRequestMetadata across a qwen-oauth hot model switch 🔁

The DashScope metadata gate reads enableRequestMetadata off
getContentGeneratorConfig(). A qwen-oauth /model switch rebuilds that object
field by field and the new flag was not in the list, so a per-model override
kept reading the previous model's value until the next full refresh.

Copy the flag and its source alongside the cache flags, and pin it with a
switch test that fails without the copy. Also drop the alicloudapi metadata
test that duplicated the existing routing test at line 553.

* fix(dashscope): read enableRequestMetadata off the provider config first 🎯

A side-model generator is built with its own per-model config but shares
the session Config, and the metadata gate only read the session value, so
a per-model opt-out never reached it and an ambient true still shipped
metadata to a backend that rejects it. Consult the provider's own config
first and fall back to the session. On the main route both are the same
object, which the qwen-oauth hot switch mutates in place, so nothing is
latched.

Pin it with two provider-vs-session cases that fail without the change,
and a vision-path case whose configured model and request model disagree
so the call site's model argument is what the gate sees. The settings row
now describes the provider-first read instead of the session-scoped one.

* fix(dashscope): read enableRequestMetadata from the provider config alone 🔒

The session fallback added last round re-admitted a value that
buildAgentContentGeneratorConfig deliberately clears for a cross-provider
agent, so the main model's setting could decide a side query on a
vendor-forwarded model. Read only the provider's own config; on the main
route that is the same object the hot switch mutates, and side generators
already carry their per-model value or a deliberate undefined.

The knob tests now pass the flag on the constructor config, the production
shape, and a new case pins that a provider config with no value ignores a
session true. It fails with the fallback in place and passes without it.

* docs(settings): say the metadata switch is read per model, not from the session 📖

* test(dashscope): pin the session-false direction and tighten two descriptions 🧭

A same-provider subagent inherits the parent config, so the settings row
now limits the isolation claim to a side model under a different provider.
The hot-switch test comment names the provider's own config as the read
path. A new case pins that a session false does not strip metadata when
the provider config carries no value.

* fix(models): let a side model decide enableRequestMetadata for itself 🧬

A same-auth-type side model built from a registry entry inherited the main
model's explicit enableRequestMetadata, so a first-party main model set to
true made every vendor-forwarded side query ship the DashScope tracing
object and fail with the 400 this switch exists to avoid. Treat the field
like thinkingMandatory in applyResolvedModelConfig: it follows the side
model, and an unset side model falls back to the automatic gate.

Pinned by a same-provider inheritance test that fails without the change.
The settings docs gain two notes: an unset side model does not inherit the
main model's explicit value (a fastModel absent from modelProviders still
does), and the switch has no effect under Qwen OAuth, where the whole
model.generationConfig block is not applied.

* docs(settings): an unlisted fastModel runs on the main model, it inherits nothing 📎
2026-09-13 03:43:09 +00:00
d47fa0202d fix(core): retry status-less upstream errors instead of ending the turn (#11291)
* fix(core): retry status-less upstream errors instead of ending the turn

A gateway that fails mid-stream — pushing `{"error": {...}}` into an
already-200 SSE response — reaches us as an OpenAI SDK APIError carrying no
HTTP status: the body's `code` and `message`, plus the response's
`x-request-id`. Observed in the wild as `code: 'KeyError'`,
`message: "'id'"`, a server-side defect that ended a four-hour turn on the
first attempt, while socket cuts on the same session were retried without
incident.

The retry classifier had no branch for that shape, so it fell through to
'unknown', and both status-less gates — the default predicate and the stream
path's inline one — matched transport codes only. They now defer to the
classification's verdict, and a status-less error carrying the provider's own
request id classifies as retryable.

The request id is the whole gate. Keying on the provider `code` instead would
sweep in permanent local failures that carry a string code and no status — a
missing API key, an invalid MCP server config, MCP's numeric JSON-RPC codes —
and walk them through the full retry ladder. Only an upstream request id
evidences that the provider accepted and traced the request before failing.
The SDK spells that field `requestID`, which the classifier did not read at
all, so it is now recognized alongside `request_id`/`requestId`.

Retries stay bounded: persistent mode still requires a 429/529 status, model
fallback still requires a status, and mid-stream replay after partial output
still requires a transport classification — so a status-less upstream error
gets the normal seven attempts and can never duplicate user-visible text.

Tests pin the observed shape both as SDK properties and as a provider body
embedded in the message, drive both gates, and guard the two verdicts that
must not move: a code-only local failure and a traced 4xx.

* fix(core): honour the retry verdict where mid-stream errors actually land

Review round on #11291. The status-less upstream class the previous commit
taught to the classifier never reached either gate it widened: both run while
the stream is being established, and the OpenAI SDK builds that error inside
the lazy SSE iterator, after establishment has already resolved. The mid-stream
catch computed the classification and then ignored it — its only retry exits
required a socket code — so the reported turn still died on the first attempt.

The catch now admits the class to both recovery mechanisms beside the transport
one, through a predicate of its own: replay when nothing user-visible has been
delivered, continuation from the delivered text once it has. Both keep the
existing budgets and every no-duplication precondition, and the socket
allow-list is untouched because the Anthropic generator shares it to decide
whether deferred tool-call chunks may be released.

A request id separates upstream from local, not transient from permanent, so
the branch now fails fast on the provider codes that can never succeed on a
resend — moderation, credentials/billing, malformed request — matched case- and
separator-insensitively, while an unrecognised code such as the observed
KeyError stays retryable. An empty request id no longer opens the gate: the
header can be present and empty, and the value is now load-bearing.

The status-less verdict is computed once and shared by both establishment
gates, which had each copied it, and an SSE-delivered instance now reports the
same kind as its siblings. The comments that described the classification as
driving one control decision name the decisions it actually drives.

Tests drive the mid-stream path by throwing from the generator's first next()
instead of rejecting establishment, and cover the continuation path, the
permanent rejection that must still propagate, the transport-over-request-id
ordering, and both empty-id readers. Each was mutation-checked: removing the
production change it pins turns it red.

* test(core): stop leaving the new rejection fixtures unhandled

CI failed the unit job on three unhandled rejections while all 23,749 tests
passed. All three came from tests the previous commit added.

Two were in the retry predicate cases: advancing fake timers between creating
the promise and asserting on it left the rejection with no handler for a tick.
The advance is gone and the assertion attaches immediately, matching the other
non-retry cases in the file — the crispness it bought on a regression is not
worth an unhandled rejection, and the case still reds, by test timeout.

The third drove a stream helper that builds its collector, awaits two timer
steps, and only then hands the collector back, so a stream rejecting in between
is briefly unhandled. That test now drains through the helper that catches
inside the loop, and drops fake timers it never needed: nothing retries on the
path it asserts, so there is no backoff to advance through.

Both still discriminate. With the permanent-code list disabled the retry case
reports three calls instead of one, and the mid-stream case reports a second
generator call in about a second rather than hanging.

* fix(core): keep oversized and malformed rejections out of the new retry gate

Third review round on #11291. Widening the mid-stream recovery gate exposed an
ordering fault of its own: the gate is consulted before the one-shot overflow
recovery below it, so a status-less input-length rejection was re-sent instead
of compacted, and the continuation arm re-sent it strictly larger each time.
Both overflow detectors are now computed above the gate and their hits excluded
from the widened verdict — from that verdict only, so a socket cut keeps the
verdict it had before this branch existed.

A request id separates upstream from local but says nothing about permanence,
and the permanence list read only `code`. The OpenAI SDK puts
`invalid_request_error` on `.type` with `code` null, so the canonical
malformed-request body never matched and a rejection that cannot succeed was
re-sent through the whole ladder. `.type` is now a second input to that guard,
matched against the same anchored list, and kept out of the classification
struct because `providerCode` is compared by exact equality elsewhere.

The list also grows the codes the previous round measured by name — billing
exhaustion, model access, output moderation, the OpenAI malformed-parameter
spelling, and oversized input — while staying anchored on purpose: a sub-code
qualifier left unmatched costs a wasted ladder, whereas over-matching costs a
transient failure that is never retried, which is the defect this branch exists
to remove.

The three stream-recovery logs now carry the classification's reason, provider
code and request id. They previously recorded only a kind and a socket code, so
the class this PR recovers logged a label pointing at sockets and proxies that
were never involved, and discarded the one field a gateway ticket can be filed
against.

Two comments that this branch falsified are corrected: the Anthropic release
gate no longer claims parity with a replay boundary that has since widened, and
the `sse-provider` gloss no longer claims the label means "delivered by the
stream" when it records raw framing surviving into the message.

Tests: a status-less overflow must reach compaction rather than replay, a
permanent `.type` must fail fast while `server_error` stays retryable, the
extended code list, and an oracle that drives the real SDK constructor with
real headers so a dependency bump renaming `requestID` goes red instead of
silently reintroducing the incident. Each was mutation-checked in both
directions where the assertion could be satisfied by accident.

* fix(core): stop resuming answers that already delivered a finish reason

Fourth review round on #11291. The continuation arm the previous round
widened to status-less upstream errors had no completeness precondition:
a gateway error frame that lands after the chunk carrying a terminal
finishReason — the SDK's error scan is position-independent, and the
pipeline keeps pulling the iterator to absorb trailing usage metadata —
was resumed as if the answer had been cut. The resume instruction
asserted a connection drop that never happened, and the fabricated tail
was folded into durable history and the JSONL record. The gate now only
continues an attempt that ended without a finish reason, or with
MAX_TOKENS — the truncated shape the arm exists for.

The remaining round-4 findings were pinning gaps, each closed with a
test and mutation-checked: the payload-overflow half of the R3-1
exclusion (a status-less 413 reason phrase now has a compaction case of
its own), the classifier fields on the continuation-scheduled and
retry-not-taken logs, the Anthropic release gate's deliberate narrowness
for a traced status-less error, the `.type` permanence guard driven
through the real SDK constructor, and the message-scraped half of the
permanent-provider-code read — both the SSE-framed body and the nested
`.error.code` shape.

* fix(core): accept answers that completed before a trailing stream failure

The completeness guard keyed on lastFinishReason could never fire on the
real throw path: the OpenAI pipeline parks the finish chunk for the usage
merge and discarded it when the iterator threw, so a gateway error frame
landing after a finished answer looked identical to a cut mid-answer.

Flush the parked finish response ahead of the propagated error so the
completion signal reaches LlmChat, and decide the post-completion case
explicitly: a stream that fails after a closed finish reason (STOP,
SAFETY, ...) with content already delivered is accepted as the turn's
outcome and persisted, instead of failing the turn or fabricating a
continuation tail into durable history. The veto is now a deny-list of
closed reasons so unmapped values (FINISH_REASON_UNSPECIFIED) fail open
to continuable, and the not-taken log names the terminal finish reason
as the decline cause.

* test(core): annotate closure-captured results array in pipeline test

The array is pushed from inside an async closure, so the evolving-any
inference does not apply and tsc --build rejected it under noImplicitAny
(TS7034/TS7005). Give it the explicit GenerateContentResponse[] type used
elsewhere in the file.

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

* fix(core): align parked-finish flush and Anthropic release gate with LlmChat recovery

* fix(core): cast pendingFinishResponse at error-path tool-call check

The deterministic gate rejected 7f7e010e99: packages/core failed tsc
with TS2339/TS7006 at pipeline.ts:747-749. TypeScript control-flow
analysis narrows pendingFinishResponse to null at the catch block (its
only assignments sit inside the handleChunkMerging callback), so the
new parkedHasToolCall property access read type never. Apply the same
explicit cast this function already uses in the finishYielded merge
above. Runtime behavior is unchanged.

* fix(core): release parked tool-call finish once content was delivered

The error-path flush withheld a parked finish carrying a functionCall
unconditionally, but the transport replay gate it protects is already
shut once user-visible content has been delivered. On the ordinary
agent-turn shape (prose, then a parked tool_calls finish, then a
gateway error frame) the withhold stranded the model's decided tool
call: LlmChat saw delivered prose, no functionCall, and no finish
reason, so the continuation arm resumed over the prose and the turn
was recorded as a fabricated success. Withhold only while nothing
visible was delivered (replay still open); release afterwards so the
delivered call shuts continuation and error-path persistence plus the
scheduler's repair flow take over — the Anthropic footing.

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

* fix(core): release parked tool-call finish on continuation attempts

* fix(core): stop post-completion gates from falsifying turn outcomes

- The trailing-failure acceptance gate nulled streamError for every
  error class, converting user cancels, pipeline InvalidStreamError
  rejections, and throttling StreamContentErrors into accepted
  completions. Accept only the failure classes the transport recovery
  gates themselves admit (socket cuts and status-less upstream frames).
- On tool-result sends the continuation veto never saw a closed finish
  reason: processStreamResponse defers the reason off yielded chunks and
  a failed attempt never re-emits it. Record the observed close per
  attempt and let the veto and the not-taken log consult it, so a closed
  answer no longer earns a fabricated continuation tail.

* fix(core): classify a trailing stream failure with the send loop's context

The gate that keeps a completed answer from failing on a trailing stream
error decides by classification, but it classified without the auth type
and configured retry codes that every other classification in the send
loop passes. A provider code only the caller's configured
`retryErrorCodes` mark as throttling carries a request id and no status,
so it read as a status-less upstream frame — the one class this gate
accepts — and a throttled turn was certified as a completed one instead
of reaching the rate-limit retry.

* fix(core): raise the pipeline's delivery flags before suspending

Two yield sites recorded what they had delivered after the yield, so a
consumer that throws into the generator while it is suspended there never
ran the assignment. The error-path flush re-tests `finishYielded` before
deciding whether a parked finish still needs delivering, and it therefore
handed the same response object back a second time, folding every part it
carries into the persisted turn twice.

The held-parts flush also yielded without updating `contentYielded` at all.
Held parts can carry non-thought content, which LlmChat counts as delivered
and which shuts its transport replay gate; with the two flags disagreeing,
the flush withheld a parked tool call that no recovery arm could pick up
any more. Held parts that are thought-only still leave the flag false, so
the withhold keeps protecting the gate it exists for.

Both are latent rather than live: nothing in production initiates a throw
into this chain, the two `.throw()` sites in it only forward one, and a
`for await` consumer abandons through `.return()`, which does not run the
catch.

* test(core): pin the Anthropic status-less path to the real SDK shape

The Anthropic release gate's status-less disjunct was justified as putting
the two providers on one footing, but the only fixture behind it hand-built
an OpenAI-shaped error, so nothing showed which Anthropic failures can
actually reach that branch. Drive the SDK's own mid-stream constructor
instead: it short-circuits on the missing status and builds an
APIConnectionError without headers, so the response's request-id never
becomes `request_id`, while the raw frame survives as the message. An id
relayed inside that body still opens the branch, and a permanent Anthropic
`type` in the same body fails fast before the request-id branch can see it.
Scope the gate's comment to that narrower set of producers.

* fix(core): scope the acceptance gate to the turn, fail fast on auth types

Two fails-closed defects from round 11.

The trailing-failure acceptance gate measured completeness with the
attempt's own visible text. On a continuation the text already delivered
lives in the turn-scoped prefix, and the merge that folds it back in runs
below the gate and only once the error is cleared, so an attempt that closed
a continuation without adding prose was refused — and no other arm owns that
failure: replay needs an empty prefix, continuation is vetoed by the same
close, and the rate-limit, overflow and invalid-stream arms do not match a
status-less frame. The turn threw, and the prose the caller watched stream
reached neither history nor the JSONL record. The gate now reads the
turn-scoped text and shares the validation block's own completeness
bindings, so it cannot accept a turn that block would then reject — which is
what keeps the post-tool-result quiet shape, and an attempt that delivered
nothing of its own, on the paths they have today.

The permanent provider-code list caught only `invalid_request_error` out of
the pinned Anthropic error-type union, so a gateway relaying
`authentication_error`, `permission_error`, `not_found_error` or
`billing_error` into an already-200 stream classified as retryable and walked
the whole ladder for a verdict fixed on arrival. Those are the types the SDK
maps to 401/403/404, all of which the HTTP-status branch already fails fast
on; `request_too_large` joins the oversized-payload codes already here. The
union's transient members stay out and are now pinned in both directions.

* test(core): pin the quiet tool-result close the acceptance gate declines

The gate's `lacksVisibleToolResultProgress` term keeps a tool-result
continuation that closed without new visible text off the acceptance path.
Whether that strands a completed answer was raised on review and is not
answerable from the code alone, so pin the measurement: with no trailing
error on the closing attempt, the same shape retries under #7039 and the
prose an earlier attempt delivered is discarded anyway, because the fresh
restart drops the continuation prefix. The discard is therefore the
quiet-tool-result policy's and not the gate's, and the asymmetry that made
the non-continuation case a defect — there the identical attempt without
the error is accepted and persisted — does not hold on this shape.

* docs(core): replace two measured-false warrants in the retry verdict

Both comments justified code by claiming what a mutation would do, and both
claims were wrong in the direction that matters — they overstated how much
depended on the code they described.

The shared verdict's doc said deleting its rate-limit term would silently
stop retrying throttling on every non-streaming path. It would not:
`classifyRetryError` runs the same predicate with the same extra codes, so
every throttle still classifies retryable without the term. Measured, the
term's only effect is on DashScope allocated-quota exhaustion surfacing as
HTTP 429, where it keeps the verdict retryable so the persistent loop's own
fail-fast bound sets the attempt count (3) instead of the first attempt
being the last. `defaultShouldRetry`'s comment repeated the same warrant.

The message-scraped provider-code test said its fixture was the only pin on
that reader and that removing it would leave the suite green. Seven other
cases in the file read the same half and fail with it.

Comment-only: no behaviour changes, and the mutations behind each claim
were reverted and the suites re-run green.

* fix(core): give two unowned trailing-failure shapes an arm that recovers

Round 15 filed two fails-closed regressions, both shapes where the
closed-finish veto and the acceptance gate together left a trailing failure
with no owner, so a turn whose prose the caller had already watched stream
died on a transport artefact and reached neither durable layer.

A continuation attempt that closes with no parts of its own — a bare finish
chunk, which is what a model returns when asked to resume an answer it
considers complete — now stays continuable. The veto exists to stop a
fabricated tail, and fabricating one needs something to fabricate onto, so
scoping it to an attempt that closed with output of its own keeps that
protection and stops it stranding the prefix. The not-taken log reads the
same binding, so it no longer attributes a stop to a finish reason that was
not the operative cause.

On a tool-result send the gate's progress term is now turn-scoped, like its
completeness sibling already was. A quiet post-tool-result close has no text
of its own, and declining on the attempt's measure left that shape owned by
nothing: the validation block which throws NO_TOOL_RESULT_PROGRESS and rides
the #7039 retry is guarded on `streamError === null`, so it was skipped while
the error stood, and replay was shut by the delivered prose. Accepting hands
the turn to that block, which recovers it on the same third attempt the
identical shape reaches one frame earlier when no trailing error arrives. The
hoisted binding the validation block reads stays attempt-local — that is what
makes the quiet close an invalid stream in the first place.

The round-10 witness for the observed-close mirror is re-pointed to a 5xx
trailing failure, which the gate's allow-list does not admit, so the veto is
again the operative cause; it still reds when the mirror is removed. The
witness that pinned the attempt-local progress term now pins the mechanism
that matters — the delivered prefix surviving into the record — because the
shape it asserted propagation for is now recovered.

* fix(core): close two ways a permanent or undelivered shape was misread

Round 16 filed two certifies-falsely/fails-closed regressions.

The permanence guard read `type` from only one of the two sources it reads
`code` from. `getRateLimitErrorDetails` collapses a scraped body's `code`
and `type` into a single `providerCode`, so on the message-embedded route a
permanent `type` was dropped whenever a sibling `code` survived — a gateway
relaying `type: 'content_filter'` beside its own `code` was marked retryable,
which is the exact rejection the permanence list exists to fail fast on. The
details struct now carries `providerType` beside `providerCode` rather than
folding the two, because `providerCode` is compared by exact equality for
allocated-quota exhaustion and pinned as that carrier.

The pipeline's error-path flush releases a parked tool-call finish on its own
view of what was delivered, which counts chunks LlmChat's protocol-tag
suppression still withholds — a leading-JSON first chunk, for one. When the
two disagreed, the released functionCall was the first thing the send loop had
seen, so counting it flipped the delivered flags and shut a replay gate that
was still open: a cut the replay arm could have recovered killed the turn on
one attempt. The flush now tags a released tool call, and the send loop — the
only place that knows what reached the caller — drops one that has no
delivered content and no continuation prefix behind it, leaving replay live.
Tagging only the tool-call release keeps a flushed STOP finish on the path
the acceptance gate's `closedFinishReason` depends on.

* fix(core): close a gate regression and an unguarded flush on cancellation

R17-1: The acceptance gate's progress term was turn-scoped (reading
`completedText`, which merges the prefix from earlier attempts), so a
tool-result continuation attempt closing with only a thought part plus
STOP and a trailing error was accepted -- the validation block then threw
NO_TOOL_RESULT_PROGRESS and the invalid-stream arm's fresh restart lost
the prefix.  R15-1's veto scoping (`attemptClosedWithOwnOutput` requires
`streamYieldedContentChunk`) made the continuation arm available for
thought-only shapes, invalidating the premise that "no arm owns that
shape".  Revert the gate to the attempt-local binding: the gate declines,
the continuation arm fires, and the prefix is preserved.

R17-2: The error-path flush in pipeline.ts had no abort guard, so a user
cancellation delivered a parked functionCall into the cancelled turn.
Cancellation persistence then wrote a model[functionCall] turn into
history and the JSONL record for a call the turn driver never dispatched.
Add `request.config?.abortSignal?.aborted !== true`, spelled exactly as
the three sibling synthesis branches in the same catch spell it.

---------

Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: qwen-code-ci-bot <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-09-13 02:15:03 +00:00
易良andQwen-Coder b5567bb7a9 fix(core): run web terminal PTYs on the bundled ConPTY backend (#11643)
* fix(core): run web terminal PTYs on the bundled ConPTY backend

The web terminal spawned its PTYs with node-pty's default inbox ConPTY
backend, where a natural shell exit orphans the conhost.exe --headless
the backend spawned: the native exit watcher erases the pty baton before
delivering onExit, pty_baton has no destructor, and the erased HPCON is
unreachable from JS (microsoft/node-pty#965). Every exited web terminal
leaked ~8 MB for the life of the CLI, and a pin bump does not fix it.

Mirror the #11497 shell path: spawn with useConptyDll on Windows so the
bundled backend releases its host reference right after spawn and the
host exits with its last client. The bundled backend adds one
synchronous throw point — a missing or unloadable conpty.dll — and a
web terminal has no child_process fallback, so a failed bundled spawn
retries once on the inbox backend: the pre-fix leaking behavior beats a
terminal that cannot start at all. A failed spawn produced no child, so
the retry cannot double-spawn. The 4001 non-retryable close path is
unchanged.

Also switch the agent-view PTY host to the bundled backend (same leak,
lower rate), refresh the conpty-host release docs, and add a
VERIFIED_NODE_PTY pin test so a node-pty bump forces re-verification of
the native teardown semantics the release path depends on.

Refs #11352

* fix(core): derive node-pty pins and widen the conpty-host pin guard

The six `@lydell/node-pty*` pins live in three unsynchronized places — the
root manifest, packages/core's manifest, and a hardcoded map in
scripts/prepare-package.js — but the conpty-host tripwire asserted only
one key of one manifest, so bumping the win32 prebuild keys or the root
manifest left it green and skipped the human re-check it exists to force.

Derive the six pins in writeDistPackageJson from packages/core's manifest
(deleting the third hardcoded table), and widen the tripwire to assert all
six core keys plus an equality against the root manifest's six — the
declaration packages/cli's agent-view actually resolves in a dev tree.

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

* fix(core): answer and scrub bundled-backend terminal queries in the web terminal

Switching the web-terminal registry to the bundled ConPTY backend let a
probing shell's DA / DSR query bytes into the recorded scrollback; a
reconnect that replays the buffer made the client's xterm.js re-answer
each query and write the fresh reply back into the still-live shell's
stdin. The bundled backend answers no queries itself, and the browser is
not guaranteed to be attached when the startup probe fires.

Mirror the other half of the shell path (#11497): on win32, feed a
headless terminal the PTY stream so it answers the probe server-side, and
strip the query bytes from both the scrollback and the live stream so a
replay cannot re-emit them. Also bring the releasePtyResources docstring
in line with the bundled backend (R1-8).

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

* fix(core): close query-strip gaps and a race in the web terminal

Three review findings on the bundled-ConPTY web terminal path:

- The scrollback scrub was a stateless per-chunk regex over only the `c`/`n`
  finals, so the query families xterm.js answers beyond those (tertiary DA,
  DECRQM, DECRQSS, XTVERSION, DECREQTPARM, OSC colour queries) still reached
  the scrollback and were re-answered into the live shell on reconnect. Replace
  it with a stateful stripper covering every family and carrying a probe split
  across two chunks.

- resize() resized only session.pty, leaving the headless query responder on
  its 80x24 spawn grid, so geometry-dependent replies were computed against
  the wrong size. Forward the resize to session.queryTerminal.

- loadXtermHeadless() was a second suspension point whose cancelledCreations/
  disposed guards were not re-checked, so a release()/releaseWorkspace()/
  dispose() landing during the import leaked a PTY the caller had cancelled.
  Re-check both after the import.

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

* fix(core): strip DCS DECRQSS queries in the terminal query filter

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

Patrol-Run: qwen-pr-closeout/jmty6iqs0zv

* fix(core): bound the terminal query stripper and tie it to its responder

The stateful stripper held any unterminated OSC (a title SET included) with no cap, so one stray ESC ] swallowed every later chunk and grew pending unbounded; the DCS alternative also required the full introducer, leaking its halves that reassemble into a re-answered DECRQSS. Hold only viable query prefixes, cap the hold, end an OSC partial at a bare ESC (accepting the C1 ST terminator), hold the DCS introducer, and gate the stripper on the responder so a failed headless load no longer strips with nobody left to answer. Correct the query-set doc comment.

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

* style(core): format web-terminal-registry.ts with prettier

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

* fix(web-terminal): stop scrubbing the OSC colour family the server cannot answer

The stateful stripper added here removed the OSC 10/11/12/4 colour queries from
both the scrollback and the live listener, but the pinned @xterm/headless 5.5.0
responder answers none of them: `onData` carries DA/DSR/DECRQM/DECRQSS only,
while `_setOrReportSpecialColor` reports on the internal `_onColor` emitter that
the headless Terminal does not expose (`term.onColor` is undefined) and that
nothing in this file subscribes to. Deleting the family from the browser's
stream as well left a probing program unanswered, where the browser's own
xterm.js 6.0.0 `_handleColorEvent` is the answerer and answered it at the merge
base.

Narrow both regexes to the families the responder actually answers (plus the
never-display-content DA3 / XTVERSION / DECREQTPARM trio that no build answers),
drop the half-covered colour arm from the partial matcher — so the strip set and
the doc block no longer disagree about what the colour family is — and correct
the doc block to state the split. Covered by a new test asserting the whole
family reaches both the live listener and the snapshot untouched, and that
nothing is written back; it fails if the colour arm is restored.

The colour queries that a reconnect replay re-answers are part of the
replay-suppression redesign tracked in #11734.

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

* fix(web-terminal): separate live replies from reconnect history

Preserve raw PTY bytes and limit the Windows server responder to primary DA. Mark snapshots explicitly and restore history into a detached browser terminal while the old terminal continues forwarding user input. Activate live replies at the snapshot write callback, reject incompatible peers, and preserve the parallel color-query regression fix. Refs #11734.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-09-13 00:03:51 +00:00
Yan ShenandShaojin Wen 4f56430138 fix(lsp): synchronize target documents before queries (#11443)
* fix(lsp): synchronize target documents before queries

* fix(lsp): address document synchronization review findings

* fix(lsp): address second-round document synchronization review findings

Evict an un-synchronizable document where the throw is raised, so one
deleted or unsupported file stops being fatal for the rest of the
connection, while workspace synchronization failures still escape the
ordinary pull-request catch instead of returning partial success.

Certify a prepare result that points at another file by observing that
file inside the synchronous signing loop, where no await separates the
response from the observation. The documented prepare-then-traverse
workflow no longer fails closed on a fresh cross-file item, and it costs
no notification, no settle delay and no second round trip. Nested items
whose own file drifted stay unsigned without dropping healthy siblings,
non-file items report non-retryable guidance, and tokens bind a text
digest plus delivered version rather than the whole document.

An unreadable warmup probe no longer cancels workspace symbol search, and
the manager only blames the synchronization callback when the server's
capabilities really cannot accept a notification.

Both design docs now define each mutation gate inline instead of citing
identifiers that resolve to nothing in the tree.

* fix(lsp): resolve R3 review findings and fix replayUris memory leak

- Fix memory leak: add replayUris.clear() in stop()
- Resolve all 17 R3 review findings (3 Critical + 14 Suggestion)
- Fix TypeScript crash restart document replay
- Improve error messages with specific file URIs
- Add pin tests for all fixes

* fix(lsp): preserve durable replay obligations across connection change and reload

Park wiped URIs into replayUris before clearing openedDocuments on a
connection change, fold the durable set into the reload snapshot and the
workspace-sweep tracked set, and evict a never-delivered unreadable URI so
one permanently unreadable file cannot wedge later sweeps.

* fix(core): preserve LSP replay and hierarchy provenance

---------

Co-authored-by: Shaojin Wen <[email protected]>
2026-09-12 23:53:08 +00:00
易良andyiliang114 b4d61e3a0b test(web-shell): give the 200-record history-viewport case its own budget (#11740)
The variant runs the same assertions under Emulation.setCPUThrottlingRate
4x over a 2,400-record fixture, so it consumes 75-91% of the shared 60s
Playwright timeout: 45,172 / 47,209 / 53,985 / 54,697 ms across four
recent CI runs, and on run 34605772996 all three attempts timed out
(63,825 / 63,081 / 62,428 ms), reding the smoke job of an unrelated PR.

Give the variant 120s, next to the throttle that makes it slow, following
web-shell.stream-performance.spec.ts. The throttle itself is the
slow-rendering condition the reading-row assertion exists to prove, so it
stays as is.

Refs #11736

Co-authored-by: yiliang114 <[email protected]>
2026-09-12 23:46:13 +00:00
Shaojin Wen b9eb29fdd1 feat(web-shell): add model role and context window configuration (#11342)
* feat(web-shell): add model role and context window configuration

* fix(cli): mock image settings in worktree restore tests

* fix(web-shell): Capture reading position before history pagination

* test(cli): align voice route metadata expectations

* fix(web-shell): preserve model configuration across updates

* fix(web-shell): preserve reconnects and synchronize model writes

* fix(web-shell): reconcile model settings and runtime feedback

* test(core): configure image model for code mode scheduler

* fix(web-shell): validate surviving model roles and preset migrations
2026-09-12 23:39:32 +00:00
qqqys 00d86315c8 feat(goal): rerun a stalled checkpoint in batches, smaller after each stall (#11690)
* feat(goal): rerun a stalled checkpoint in batches, smaller after each stall

A checkpoint that stalls on an overflowing window leaves the cursor where
it was, so the next attempt rebuilt the same window and sent the verifier
the identical request at temperature 0, failing the same way until the
stall breaker stopped the Goal.

The first check on a window still sends it whole. A check that starts with
a stall streak splits the evidence into consecutive batches (24 records
after one stall, 12 after two) and calls the verifier once per batch:

- each batch's claims are carried into the next as previousClaims, and
  only the last batch's checkpoint is kept and moves the cursor;
- intermediate batches use their own claim id segment (<uuid>:b<k>:<n>);
- a failing batch ends the check through the existing arms, and the
  recorded failure names the batch ("batch 2/5: ...") inside the cap;
- a restore replay batches by its restored streak and still spends no
  stall.

Docs describe the batching and its worst-case duration, and the
goalCheckpointTimeoutSeconds description says the ceiling is per call.

* fix(goal): split only an overflowing live checkpoint, not a room window or a replay

A window with room settles a failed check as inconclusive and a restore
replay is exempt from the stall breaker, so neither can spend a stall and
splitting them bought nothing: it only added verifier calls and lossy
per-batch recompression, and for a replay held up session activation,
which waits for the replay to finish. Both now go out in one call as they
did before; only a live check on an overflowing window after a stall is
split.
2026-09-12 21:45:46 +00:00
qqqys ba3a51e71d feat(workflows): add effort and disallowedTools to agent() options (#11691)
* feat(workflows): add effort and disallowedTools to agent() options

A workflow script can now set the reasoning effort for one agent and take
tools away from it. Every agent previously ran at the session's effort with
the full tool set minus a fixed floor, so a script paid high-effort cost on
cheap stages or ran its judges at low effort, and a read-only stage could
still edit files.

agent({ effort }) accepts low, medium, high, xhigh or max with the aliases
/effort accepts, and omitting it inherits the session tier. The tier goes
onto the agent's own copy of the content-generator config, never the
session's, limited to the tiers /effort offers for that model: a tier the
model does not offer becomes the next stronger one it does, and a model
offering none keeps the inherited tier. It is written by the session
setter's logic, now one shared helper, and providers still clamp it per
request. An effort-only dispatch gets its own runtime view for that reason.

agent({ disallowedTools }) is unioned with the workflow floor and the agent
type's own denies, so it only narrows. Display names resolve through the
spawn's normalizer. A schema agent that denies structured_output is refused
before it spawns.

Both options join the resume key. The sandbox validates and normalizes them
on the revived copy that crosses to the host (alias to tier, deny list
sorted and de-duplicated), so equivalent spellings replay and different
requests run live. The workflow-authoring skill documents both.

Part of #11013

* fix(workflows): make per-agent effort and deny lists hold

Address the first review round of the agent() effort and
disallowedTools options.

Effort: an explicit tier now drops a thinking budget the agent's copy
inherited, never re-enables thinking the session turned off (also across
a provider switch), finds the declared tiers behind a gateway base URL,
and falls back to the provider's built-in tier table when the registry
declares none. An effort-only dispatch builds its own generator only
when the tier can land, refuses to open an interactive login for it, and
runs on the session's generator if that build fails. The CLI's /effort
tier rule now delegates to the same core helper.

Deny lists: an entry that matches no tool is refused instead of silently
denying nothing, built-in display names become tool names before the
resume key is derived, and a schema agent is refused when any deny in
the union reaching the spawn, including its agent type's own, names
structured_output. The fast path is driven by the option list the resume
key projects.

Sandbox: the phase is recorded only after the option gates pass, and an
echoed effort value is stripped of control characters. The review
harness mirror accepts the two options, and the authoring skill states
where effort does not reach.

Part of #11013
2026-09-12 21:44:12 +00:00
易良andQwen-Coder 93c0d6d20d fix(core): guard internal git calls against configured helper programs (#11669)
* fix(core): stop a repository's own git config from running programs during automatic git calls

A repository can name a program in its own .git/config and have git run it
on the next index refresh. The git calls the agent makes on its own
initiative reached that: startup context, the daemon's working-tree status
and diffs, the ignore probe, the grep tool, the team-memory sync, the
stale-worktree sweep, and the two worktree dialogs.

Guard at the chokepoints (the simple-git factory, gitDiff's runGit, the
team-memory and orphan-cleanup spawn points) with -c core.fsmonitor=, and
pass --no-ext-diff --no-textconv on the diff calls for the two
program-valued keys that config cannot empty.

* test(core): mock execFileSync in chatRecordingService tests

getGitBranch now calls execFileSync instead of execSync, so the tests' node:child_process mock must stub execFileSync to keep gitBranch resolving to 'main'.

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

Patrol-Run: qwen-pr-conflict/jmtx6sqi9yb

* fix: close post-index-change on guarded status probes and pin remaining guards

Address review feedback on the fsmonitor guard:

- Add --no-optional-locks to the three guarded `status --porcelain` probes
  (isDirtyTree, checkoutHasWork, WorktreeExitDialog) so they no longer write
  the index and run a tree-shipped `post-index-change` hook.
- Migrate chatRecordingService.autoTitle.test.ts to execFileSync and pin
  gitBranch so a stale mock target surfaces as undefined.
- Add a clean-tree `false` assertion to the hasTrackedChanges canary so it
  distinguishes a real status read from the fail-closed `true`.
- Pin the checkoutRef guard with a core.fsmonitor canary case, and add a
  post-index-change canary + control for isDirtyTree.

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

* fix(core): close remaining git config execution paths

* fix(core): carry --no-optional-locks in argv on the worktree dirty probes

hasWorktreeChanges and countWorktreeChanges passed GIT_OPTIONAL_LOCKS=0
through simple-git's two-arg .env(), which replaces the child environment
outright — dropping PATH/HOME and hiding the global core.excludesFile and
safe.directory. Run the status probe through execFile with
`-c core.fsmonitor= --no-optional-locks status --porcelain` instead, so the
guard stays in argv and the environment is inherited.

Add a canary case proving the probes read the global excludesFile (red if the
child environment is clobbered). Also correct the load-simple-git loader
comment: a leading literal global flag such as --no-optional-locks is safe
because it is a fixed token, not a caller-supplied entry.

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

* fix(core): suppress post-index-change on the team-memory status probe and roll back on rejected push

The session-start status probe in syncTeamMemory lacked --no-optional-locks,
so when the team path is clean it was the flow's only index-writing call and a
tree-shipped .git/hooks/post-index-change ran on it. Add the flag (before the
status subcommand, so git does not answer rc=129).

The --no-gpg-sign commit made a signing repo's push deterministically fail; the
rejected commit then stranded on the branch and tripped wasAheadBeforeSync,
permanently disabling push. Undo our own commit (soft reset + pathspec-limited
unstage) when the push is rejected so the branch returns to its pre-sync HEAD.

Add real-git regressions for both: a clean-path post-index-change canary, and a
pre-receive-rejecting remote asserting HEAD is restored and a second sync does
not return local-ahead.

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

* fix(core): close remaining git config exec and rollback gaps

- Team-memory push-failure rollback now rewinds by SHA: it records the
  commit it created and only resets when HEAD still points at it and the
  remote does not already have it, so a concurrent writer's commit is no
  longer dropped by a blind HEAD~1.
- The two worktree dirty probes pin `--untracked-files=all` so an ambient
  `status.showUntrackedFiles=no` (or git's default `normal` collapsing an
  untracked directory) cannot read a worktree clean, and set a 10 MiB
  maxBuffer so a large tree is not misreported as a git failure.
- Widen NO_EXEC_CONFIG / NO_EXEC_CONFIG_SETTINGS to also carry
  `log.showSignature=false`, closing the `log.showSignature` -> `gpg.program`
  vector at the two remaining `git log` paths (gitDiff runGit and the
  simple-git loader).

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

* fix(core,cli): address team-memory sync review feedback

- Qualify the clean-path status probe comment: pull --ff-only also writes the index on the upstream path.
- Scrub ambient git config in both canary tests so a system/global core.hooksPath cannot silently disable the planted hook.
- Assert the rollback leaves the index empty and working tree intact, and that a second sync commits a new note and re-reports push-failed.

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

Patrol-Run: qwen-pr-closeout/jmtyjdph70g

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-09-12 21:24:52 +00:00
qqqys 632773e018 refactor(core): apply one matcher rule to every hook event (#11619)
* refactor(core): apply one matcher rule to every hook event

Hook matchers meant different things per event and per registration
path. Notification, PreCompact, PostCompact and StopFailure accepted only
a single exact value, so `permission_prompt|idle_prompt` never matched,
and session-scoped hooks, including skill frontmatter hooks, anchored
their regex while settings hooks did not.

One shared matchesHookPattern now serves the planner and the session
hooks manager: an empty matcher, `*` or `.*` matches everything; the
matcher or any entry of a `|` list matches exactly (tool events also
compare aliases exactly); otherwise it is an unanchored regex on the
subject. Existing tool, subagent, command, session and file-path matcher
behaviour is unchanged.

Part of #11610

* fix(core): decide pipe-list matchers entry by entry

Review follow-up. A `|` list compared its entries exactly and then fell
back to the whole pattern as a regex, so `Write|*` became an invalid
regex that matched nothing, and a stray pipe such as `write_file|edit|`
became a regex that matched everything. Lists are now decided entry by
entry with the full rule; empty entries are ignored and never widen the
list, and a group spanning the pipe such as `a(b|c)` still falls back to
one regex.

The planner no longer keeps its own copy of the match-all rule. The hooks
and skills guides now say that skill matchers are unanchored, that an
omitted skill matcher matches every tool, how to anchor runtime ids and
exclusions, and that aliases only match exactly.

Part of #11610

* fix(core): decide pipe-list matcher entries without compiling them

A pipe-list entry now matches only as `*`, `.*`, or an exact subject or
alias. Entries are no longer compiled one by one, so an escaped pipe such
as `notes\|todo\.md` keeps its whole-pattern meaning instead of letting
the `todo\.md` fragment match any path, and a group that spans the pipe
no longer logs "Invalid regex" warnings for its unbalanced fragments.

The whole-pattern fall-through is compiled from the non-empty entries, so
a stray `|` is dropped rather than disabling the fall-through:
`read_(file|edit)|` matches `read_file`, while a list of only `|` still
matches nothing and a trailing `|` never becomes a match-all.

Pin that a skill hook entry without `matcher:` matches every tool.

* fix(core): trim pipe-list matcher entries and split only on unescaped pipes

Trim each list entry at the split so the exact comparison and the rebuilt
regular expression see the same entries: `read_.* | edit` now matches
`read_file` instead of compiling alternatives that demand literal spaces.

Split only on a `|` that is not preceded by a backslash, so an escaped pipe
never manufactures an empty entry or a dangling escape: `notes\|` matches
`x/notes|y`, `a\||b` matches `b`, and `foo\|bar` no longer matches `bar`.

Scope the documented stray-pipe guarantee to lists that do not start with
`^` or `(`; those matchers are compiled as written.

* fix(core): split matcher lists only at top-level pipes

The list reading of a matcher split on every pipe not preceded by a
backslash, so it cut through character classes and groups and then trimmed
spaces that belonged to the expression: `foo[ |]bar` was rebuilt as
`foo[|]bar` and stopped matching `foo bar`, and `a(b | c)` lost its spaces
the same way. A single preceding backslash also hid a real separator after
an escaped backslash, so `C:\\temp\\|D:\\data\\|` kept its trailing empty
alternative and matched everything.

Split with a scan that reads backslashes in pairs and ignores pipes inside
`[...]` and groups, so only top-level entries are trimmed, compared and
rebuilt, and `read_.* | edit` keeps reading as `read_.*|edit`. Tests pin
each shape and the `^` and `(` guards, and the hooks guide says which pipes
separate entries.
2026-09-12 21:23:40 +00:00
54aa66834b fix(core): fire active-todo reminders at delegation boundaries and user turns (#10963)
* fix(core): fire active-todo reminders at delegation boundaries and user turns

A session that delegates to foreground subagents earns one parent tool
turn per tens of minutes of real work, so the 3-turn budget of the
active-todo reminder never comes due and the persisted plan freezes
(#10953). Any ordinary user turn additionally discards a registered
reminder, so the very message asking about progress destroys the plan
context.

- Force the reminder due on a tool-result batch that carries a
  top-level Agent tool result, in both the TUI (client.ts) and ACP
  (Session.ts) paths, putting the nudge exactly where delegated
  progress arrives.
- Continue the previous work chain on an ordinary user turn while a
  reminder is still registered — a registered reminder implies
  unfinished items, since todo_write deletes it on plan completion.
  This mirrors the existing retry continuation.

The observational contract of todo_id is unchanged. Time-aware cadence
(direction c) and the delayed tool-response recording (direction e) are
separate concerns and intentionally not included.

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

* docs(design): sync active-todo-context with the two rules this PR changes

The doc still promised that an ordinary new prompt clears stale Todo state,
and described the turn cadence as the reminder's only delivery path. This PR
changes both: an ordinary turn continues the chain while a reminder is
registered, and a tool-result batch carrying a top-level Agent result forces
the reminder due because a delegated run earns the parent a single turn.

Record the accepted cost (an abandoned plan resurfaces until a later write
completes or clears it) and that the ACP todo-stop-guard lineage reset stays
keyed to the retry/continue flag, so carrying a plan never widens guard trust.

* fix(core): clear stale active-todo reminders on rewind and preserve automatic-turn owners

History rewind / restore / setHistory discard the timeline the active-todo
reminder describes, but the reminder and its work-chain owner survived, so the
next turn continued work that was rewound away. Clear the reminder state where
history is discarded (LlmClient.truncateHistory/setHistory, ACP
rewindToTurn/restoreHistory) via a new Config.clearActiveTodoReminders().

Continuing a chain also dropped still-live related automatic turns' owner
mappings (owners.clear()), so a plan completed by an in-flight automatic turn
kept its reminder and kept reporting unfinished work. Re-point instead of
clearing, dropping only entries whose mapped owner is discarded.

Extend the continuation tests to pin the cleared-reminder branch, parameterize
the ACP force test over the legacy `task` alias, and document the mid-turn
`force` case on takeActiveTodoReminder.

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

* fix(core): clear stale active-todo reminder when a non-owner completes the shared plan

Two changes close the R4-1 regression and bound the R4-2 retention:

- setActiveTodoReminder(undefined) now clears session-wide. todo_write passes
  undefined only when the list has no unfinished items, and the plan file is
  session-scoped, so a completing cron/notification/subagent/Goal turn can no
  longer strand the foreground reminder and make the next ordinary turn
  re-chain a finished plan (#10953).

- startActiveTodoWorkChain drops the superseded foreground head before
  re-pointing, so the owners map stays bounded instead of growing one entry
  per continuation. Still-live related automatic-turn mappings are preserved.

Forward setActiveTodoReminder through buildSubagentContextOverride so a
subagent's completing todo_write reaches the parent's session-scoped reminder
instead of its isolated wrapper map.

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

* docs: document the history-discard reminder clear in active-todo-context

The same PR adds a fourth reminder-clear condition (rewind / restore /
setHistory / truncateHistory) across both frontends, but the design doc still
enumerated only three clear conditions and had no Verification bullet for the
discard rule. Add the condition and the matching bullet so the enumeration
stays exhaustive.

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

* fix(core): remove dead active-todo reminder write-through in subagent context

todo_write is the only production caller of Config.setActiveTodoReminder and
is excluded from both subagents and teammates (EXCLUDED_TOOLS_FOR_SUBAGENTS /
EXCLUDED_TOOLS_FOR_TEAMMATES), so the write-through shim installed on the
derived subagent context can never run; the single-layer test beside it pinned
a layering production never builds. Todo state is parent-owned by design.

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

* test(core): pin retention loop with a non-completing automatic-turn write

The completing-write sibling clears session-wide regardless of ownership, so
it cannot discriminate the owners-map retention loop against owners.clear();
a non-completing write only lands on the shared owner when the loop keeps the
prompt-auto -> prompt-user mapping alive.

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

* style(core): format config.test.ts

* fix(core): gate active-todo carry on the last plan-writer owner

An isolated cron/notification/Goal turn that rewrites the session-scoped plan file under its own owner left the foreground's stale snapshot in place, and the continuation guard re-delivered it every turn. Record the owner of the last real todo_write and require owner equality in the continuation guard so only the plan's current owner carries the reminder.

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

* fix(cli): gate ACP todo continuation on the plan-writer owner

Port the owner-equality gate that landed in core (client.ts:3501-3510) to
the ACP continuation guard. The ACP guard carried on reminder presence
alone, so an isolated cron/notification rewrite of the session plan under
a foreign owner left the stale foreground snapshot re-delivered on every
turn and at every Agent-result boundary, diverging from core on the same
state. Extend the three ACP Config mocks and pin the foreign-owner
no-carry case with an ACP twin of the core test.

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

* fix(cli): reserve ACP turn-start todo injection for machine continuations

The ACP prompt path forced takeActiveTodoReminder(promptId, true) for every
turn that reaches its straight-line body. Before the carry rule that was inert
on an ordinary turn (the fresh-chain branch cleared the reminder first); with
the carry rule it splices up to 800 chars of model-authored plan text ahead of
the user's own text on every ordinary turn, permanently recorded in history,
while core injects at turn start only for Retry | Cron | Notification |
Teammate.

Gate the force to the turns that continue a chain as machine work (retry,
resumed interrupted turn, runtime goal continuation), keeping the restore-turn
exclusion and the retry force, and align the design doc plus both test files:
the reminder now comes back from the mock only under force, so the ordinary
turn's request assertion fails if the gate is reverted.

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-09-12 19:56:50 +00:00
bluefateludi 3ba01990e1 fix(core): keep the mirrored reasoning field off Fireworks requests (#11701)
Fireworks' OpenAI-compatible endpoint accepts the non-standard
reasoning_content field on input but rejects the additional reasoning
field with HTTP 400 (Extra inputs are not permitted, field:
'messages[N].reasoning'). The default provider mirrors
reasoning_content into reasoning for any model whose name contains
'qwen3' — a family match that does not establish the endpoint accepts
the extra field — so Qwen3 thinking models on Fireworks fail every
multi-turn continuation that replays a thinking turn, including
tool-call continuation (issue #11657).

Add a Fireworks provider, detected by api.fireworks.ai hostname in
determineProvider (following the Cerebras precedent from #11049), that
undoes the mirror at the outbound request boundary. Only the exact
mirror copy (reasoning === reasoning_content) is dropped; a distinct
explicit reasoning field and reasoning_content itself — which Fireworks
documents for reasoning replay — are preserved. Conversation history is
never mutated and non-Fireworks endpoints keep the existing mirroring.
2026-09-12 19:20:55 +00:00
易良andQwen-Coder d50dc84e36 fix(goal): remove backtracking fence unwrap from judge replies (#11693)
* fix(goal): replace backtracking stripCodeFence with linear stripMarkdownFence

The judge's fence unwrap used /^```(?:json)?\s*([\s\S]*?)\s*```$/i, whose
three overlapping quantifiers backtrack super-linearly on a reply that opens
a fence and then degenerates without closing it (measured ~49 s for 5 000
spaces), blocking the event loop past the judge's own 25 s timeout. Reuse the
index-scan stripMarkdownFence already in the checkpoint verifier instead, and
drop the judge's copy.

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

* fix(goal): parse judge replies whose payload is glued to the fence line

stripMarkdownFence's multi-line branch drops the opening fence line, so a
judge reply that puts its JSON payload on the opening line (with the closing
fence on the next line) unwraps to brace-less text and parseJudgeReply
returns an error verdict. Fall back to the raw reply when the unwrap
produced no '{', so the tolerant index-scan still finds the payload.

Also pin the judge's fence unwrap with two new regression tests and tighten
the existing degenerate-fence guard: a per-test timeout (the old backtracking
regex took ~49 s on this shape, past the runner's raised 60 s ceiling) and a
stage assertion confirming the reply actually reached parseJudgeReply.

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

* style(core): format goalJudge.test.ts with prettier

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

* fix(goal): drop dead fence unwrap from judge reply parsing

parseJudgeReply's tolerant first-`{`-to-last-`}` scan already finds the
payload for every fence shape a judge produces: markdown fences
(backticks, tildes) and their info strings carry no braces, so the scan
reaches the payload without a separate unwrap. The stripMarkdownFence
import added to replace the backtracking stripCodeFence was therefore
dead weight, and its `includes('{')` fallback latched onto a decoy brace
when the payload was glued to the opening fence line and quoted a braced
excerpt, rejecting a verdict the deleted stripCodeFence parsed. Remove
the import and the fallback; the remaining scan is already index-based
and linear, preserving the ReDoS fix. Add a witness test for the glued
pretty-printed reply quoting a braced excerpt.

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

* fix(goal): keep checkpoint fence parsing private

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-09-12 19:20:36 +00:00
易良andQwen-Coder b5c7635ff9 fix(core): compute cron next-fire time with epoch arithmetic across DST fall-back (#11723)
nextFireTime advanced its minute-by-minute scan with local Date setters
(setSeconds / setMinutes). ECMA-262 resolves an ambiguous fall-back
wall-clock to its earlier occurrence, so an anchor inside the repeated
autumn hour was re-projected up to 59 minutes into the past, breaking the
strictly-after contract, and a forward scan skipped the entire second
occurrence of the hour.

Traverse elapsed time instead: initialise the candidate with epoch
arithmetic (floor(after/60000)*60000 + 60000) and advance with
candidate.setTime(candidate.getTime() + 60000), while continuing to match
cron fields with local getters. Cron syntax, jitter policy, and other
scheduler behavior are unchanged. Fixes the root cause reported in #11720.


Patrol-Run: qwen-issue-patrol/jmtygitr20b

Co-authored-by: Qwen-Coder <[email protected]>
2026-09-12 16:39:15 +00:00
1331c7af85 fix(core): reap surviving hook process trees on Windows (#11623)
* fix(core): reap surviving hook process trees on Windows

terminateSurvivingHookProcessGroup was an explicit no-op on win32, so
when a parent-exit-surviving hook (MessageDisplay / StopFailure /
SessionDelete) was cancelled, nothing reaped the shell the supervisor had
started. Those hooks spawn a detached supervisor plus a shell child and
unref() it, so the parent's own tree kill on the supervisor is not a
reliable substitute: the supervisor may already have exited, which
reparents the shell out of its tree, or its taskkill may fail. The result
is the leftover cmd.exe processes reported in #11303.

Implement the branch with taskkill /f /t, extracted from
terminateWindowsHookProcessTree as a shared pid-only helper.

Guarded by a process.kill(pid, 0) liveness probe first. Windows has no
process group to signal, so an unguarded taskkill against a pid that has
already exited could land on a recycled pid belonging to an unrelated
application — the #6067 collateral-kill failure mode. The POSIX branch
gets that safety for free by signalling the negated pid.

* fix(core): reuse shared pid liveness probe in Windows surviving-hook reap

* fix(core): warn when a surviving-hook liveness probe fails unexpectedly

An unexpected process.kill(pid, 0) errno (ENOMEM, EMFILE, libuv's
UV_UNKNOWN catch-all, ...) read as "dead" through isPidAlive, so the
Windows surviving-hook reap skipped without a trace even though the
hook's cmd.exe tree may still be running. Classify the probe at the
call site into alive/gone/unknown — the skip decision is unchanged —
and warn on the unknown case.

Also pin that the win32 branch never falls through to the POSIX
process-group path with a negative-pid assertion, and record the warn
in the design docs.

* fix(core): size code-mode host startup grace for slow machines

A code-mode execution's wall clock is timeoutMs +
CODE_MODE_HOST_STARTUP_GRACE_MS, measured from host spawn. A 5s grace
is below the observed cold boot of the host process (node + tsx +
QuickJS WASM init, ~4s unloaded and worse under parallel load) on slow
machines, so executions with a small timeoutMs were killed during boot
with a spurious "timed out" error. Raise the grace to 30s, matching
the guest timeout scale; fast machines see no change since the
deadline only binds when startup is slow.

Also lift two scheduler tests' intermediate vi.waitFor budgets (4s)
and one exec-context test's (1s default) to 30s: nested tool dispatch
cannot be observed before the host has booted, so deadlines below the
boot time failed deterministically on slow runners. Assertions are
unchanged; each test's own timeout still caps it.

* fix(core): sharpen surviving-hook probe diagnostics and code-mode timeout message

- Carry the probe errno in the surviving-hook skip warning and share the
  tri-state liveness classification with the SIGKILL-fallback re-probe,
  which now warns instead of skipping silently on an unexpected errno.
- Report the wall budget actually enforced (guest budget + host startup
  grace) when the code-mode host never frames a response.
- Raise the enclosing per-test caps above the 30s waitFor budgets so a
  genuine stall still surfaces the assertion diff, and state in the
  design docs that the probe warnings live in the debug log (--debug).

---------

Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
2026-09-12 16:36:18 +00:00
易良andQwen-Coder 85509c749d fix(core): redact error text in usage-statistics telemetry sink (#11649)
* fix(core): redact error text in usage-statistics telemetry sink

* fix(core): close telemetry error-redaction gaps

Close the highest-severity review findings on the redaction table:

- Add `error` to ERROR_TEXT_PROPERTY_KEYS so hook failure text written to
  properties['error'] is redacted at the enqueue choke point (R1-1).
- Skip any auth scheme (Basic/token/Digest/…) before the value, not only
  Bearer, so `Authorization: Basic <creds>` no longer leaks (R1-2).
- Add `key` to the env key alternation so *_API_KEY / *_ACCESS_KEY* are
  redacted (R1-6).
- Run redaction on the raw text before stripAnsiAndControl so the
  whitespace-delimited patterns still see their newline/tab delimiters (R1-7).
- Share one secret-value pattern that accepts a quote-opened value (R1-3) and
  let the flag name contain the secret keyword anywhere (R1-4).

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

* fix(core): harden telemetry error redaction patterns

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

* fix(core): run prettier on qwen-logger redaction test

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

* fix(core): close telemetry error-redaction quote/continuation/ANSI gaps

Harden the usage-statistics error-text redaction pass against the review
findings on this branch:

- Normalise shell line continuations (`\` + newline + leading whitespace)
  ahead of every pass, so a credential split across a continuation (including
  a URL credential) can't survive as a glued cleartext run.
- Make SECRET_VALUE quote-aware (a quoted run matches to its closing quote),
  so interior quotes and quoted values containing spaces redact whole instead
  of leaking a suffix after the marker.
- Give the env pattern the same quoted alternative behind its floor.
- Skip an optional leading backslash in each separator so a shell-escaped
  nested quote (`Authorization: Bearer \"secret\"`) still redacts.
- Strip ANSI/C0 per line (after widening tabs to spaces) before the patterns
  run, so an escape or control character can't hide or reassemble a credential
  the pattern pass never saw.

Fix the line-continuation test to assert the secret is absent rather than
merely that no marker is emitted, and add regression cases for interior
quotes, quoted values with spaces, ANSI/C0, and continuations.

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

* fix(core): bound secret-name quantifiers and unify flag/env value floors

- Bound the non-secret name chars around the secret keyword in
  SECRET_FLAG_PATTERN and ENV_SECRET_PATTERN (previously unbounded `*`,
  now `{0,64}`) to prevent quadratic backtracking on adversarial error
  text that runs through the synchronous telemetry redaction pass.
- Give SECRET_FLAG_PATTERN the same 10-char value floor as
  ENV_SECRET_PATTERN so `--max-tokens 8192` and `max_tokens=8192` are
  handled consistently (both pass through untouched).
- Align the ENV_SECRET_PATTERN docblock examples with the actual 10-char
  floor (the old `ghs_xxx` / `sk_xxx` examples were shorter than the floor
  and were never redacted).

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

* fix(core): drop raw error text from usage telemetry

* fix(core): drop degenerate logPrompts gate on hook error text

`redactEventErrorText` already replaces every string under `error_message` /
`error_excerpt` / top-level `message` with a fixed marker at the enqueue
boundary, so `logHookCallEvent`'s `getTelemetryLogPromptsEnabled()` gate on
`properties['error']` could only ever emit the constant `'***REDACTED***'` —
a consent flag that no longer gates any content. Hook failure is already
signalled by `success` / `exit_code`, so the raw error text is dropped
fail-closed instead, and the now-dead `'error'` entry is removed from
`ERROR_TEXT_PROPERTY_KEYS`.

The failed-hook test now drives a config with telemetry log prompts enabled
and asserts `properties` carries no `error` key, which goes red when the
gated assignment is restored.

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

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-09-12 16:35:05 +00:00
易良 94449f9dfb docs(daemon): describe the prompt rate-limit and queue-full responses separately (#11733)
* docs(daemon): split prompt queue-full from the rate-limit failure

* test(cli): floor the guide protocol-anchor scan
2026-09-12 16:17:52 +00:00