Commit graph

1775 commits

Author SHA1 Message Date
qwen-code-dev-bot
f58809c730
Merge branch 'main' into codex/model-api-selection 2026-09-12 09:24:54 +08:00
nerdalytics
fdb33117b4
feat(skills): extension skills are named by their extension (#10841)
* feat(skills) — extension skills are named by their extension (#9408)

Extension skills register as `<extension>:<name>` everywhere the registry
name is used; `authoredName` preserves the authored spelling. Restrictions
(`skills.disabled`, `skills.defaultDisabled`, `slashCommands.disabled`)
match under either spelling; `skills.enabled` matches the registry identity
only, so a rename never grants capability. The picker and the Skills dialog
name the owner, the picker guard names the settings entry that blocks a
skill, and a stale bare `skills.enabled` entry is announced at startup with
its qualified replacement. Mirrors gemini-cli#23566.

Fixes #9408

* fix(skills) — stale settings get honest answers: reason-aware warnings, locked rows naming the blocker, qualified ACP rows, column-aware badges

Review wave on #10841: the startup warnings take the raw lists (a bare
opt-in pair that cancels a defaultDisabled entry is load-bearing, not
stale) and advise by block reason (hard entries are removed, not
cancelled); the Skills dialog locks rows a toggle cannot change and names
the blocking entry instead of saving into a no-op; ACP manifest-only rows
carry the qualified name so the web shell cannot persist a bare entry that
never grants; badge and row truncation count terminal columns;
lookupSkillDisablement prefers the hard entry in mixed-spelling states;
docs, schema description, and the regenerated settings schema state the
bare-pair corner and the `_`-fold collision; delete-red tests pin the
authoredName propagations.

* fix(skills) — qualify config-only snapshot expectations; run the locked-row dialog test under jsdom

* fix(skills) — honest locks and warnings: trust-gated workspace entries, grant cancellation, scope-labeled lock rows, wired startup warnings

* fix(i18n) — translate the reworded locked-section header for zh and zh-TW

* fix(skills) — advertise registered names, restore six locales, pin list wiring

Round-3 review follow-ups: the @mention context builder now advertises
extension skills under their registered name (rust:pdf), the skills.enabled
prose states the workspace-stored enablement on all four surfaces, the lock
header returns translated in ca/de/fr/ja/pt/ru, and the settings-lists
provider wiring gains tests that go red when the mapping or the resolved
disable set is swapped.

* fix(skills) — align mention-context tests with qualified advertisement

Session.test.ts and extension-mention-ref.test.ts still pinned the bare
authored spelling in the advertised Skills line; both now expect the
registered name the builder emits.

* fix(skills) — entry-scoped grant warnings and authored-name resume restore

A bare enabled entry names every same-authored extension skill, so the
stale-grant warning now advises per entry and names the hard block that
defeats a replacement; a load-bearing pair targeting a default-off skill
names the silent flip; hard-unblock advice names the siblings a removal
re-enables; resume-restore falls back to authored names so pre-rename
transcripts keep their dedup state.

* fix(skills) — name the highest scope in lock labels, per merge precedence

The entry-collection loop inserted Workspace after System, so a workspace
entry overwrote the System label and the lock blamed the one file whose
edit cannot unlock. Iterate in true merge precedence order and pin it with
a witness test, as requested in review.

* fix(skills) — correct the R3-3 witness expectation to the entry-naming label

lockedIn returns the entry-naming string for defaultDisabled-sourced
locks, not the bare scope label, so the witness must expect
skills.defaultDisabled 'pdf' (System). The reorder itself was correct;
only the assertion shape was wrong.

* fix(skills) — hard-unblock advice never loops on same-named local skills

A skill whose registry identity equals the bare entry shares every
matching entry with the opt-in, so advising the user to add it back
re-blocks the opt-in and reprints the same warning. Name the limitation
instead of the entry, and pin it with a fixed-point test.

* fix(skills) — default-off pair warning names only true default-offs and the converging replace

* fix(skills) — pin the caller's default-off set, and no off-claim over a live grant

N-4: the default-off set initialize() feeds bareEnabledGrantWarnings had no
witness; collecting authored names kept the suite green while the pair
warning went silent at runtime. An initialize-driven test now pins it.
N-5: with a qualified grant beside the bare pair the skill is enabled, so
the pair warning split its wording: cleanup advice only where the grant
already enables, off-state wording for the members still default off.

* fix(skills) — the pair warning names its hard blockers, and the qualified guard gets its witness

* fix(skills) — the daemon toggle stops promising an enable the bare entry still forbids, and the branch grows one more commit for a prefix

doudouOUC's review at ab8e10e3 (5170856226), suggestion 1: the serve and
web shell toggle wrote skills.enabled and reported success while a
legacy bare skills.disabled or skills.defaultDisabled entry kept
blocking the grant, a no-op the TUI never offered because its row
stays locked. The lock decision moves from SkillsManagerDialog into
skill-settings.ts so the persist providers refuse the same writes the
picker refuses, and answer with the entry and scope that forbids them
instead of a success the merged config then denies.

Suggestion 2, the warning prose living in core and its i18n tail,
stays a follow-up PR. The worms stay in the can; this one lid at
least closes honest.

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: 易良 <1204183885@qq.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-09-11 23:01:15 +00:00
Shaojin Wen
38ea643638
feat(web-shell): continue interrupted sessions (#11545)
* feat(web-shell): continue interrupted sessions

* fix(web-shell): preserve continuation across reconnects

* test(web-shell): align mock context with daemon type

---------

Co-authored-by: probe <probe@local>
2026-09-11 22:32:19 +00:00
qqqys
aaa6a32aae
fix(goal): persist why a checkpoint failed and show it before the Goal stops (#11576)
* fix(goal): persist why a checkpoint failed and show it before the Goal stops

A stalled-checkpoint stop always recorded the same reason and advised
narrowing the objective, whatever the three failed checks had run into,
and nothing showed a failing checkpoint until the Goal stopped.

- Keep a capped one-line lastCheckpointFailure on the Goal record: set by
  every failed check, cleared by a check that succeeds, left alone by a
  check that proves nothing, and cleared with the stall streak on edit and
  on the resume of an evidence-limited Goal.
- Pick the stall-stop reason from the check that spent the last stall: a
  full claim list, an unusable verifier answer, or a verifier that never
  answered. limitKind stays evidence_catalog, so resume is unchanged.
- Show the streak and the failure on the Ink and OpenTUI Goal cards, the
  footer pill, the web shell Goals dialog, and get_goal's lastGoal summary.
- Tell the model to retry update_goal right after get_goal when a
  checkpoint is required, before running other tools.

Closes #11326

* fix(goal): address review on checkpoint failure diagnostics

- Read claim-budget and claim-length overruns as capacity failures that
  keep the narrow-the-objective advice, and rename that shape to capacity.
- Reword the no-answer stop so it names provider, timeout and check errors
  instead of blaming the provider, and rethrow the verifier's own timeout
  when a provider SDK replaces it with a generic abort error.
- Write the diagnostic as one display-safe line (control sequences and
  bidi overrides removed, whitespace collapsed before the cap) and sanitize
  it again at the Ink card and the web Goals dialog.
- Share one visibility rule across every surface and lastGoal: never on a
  completed Goal, always during a stall streak, and a stall-free failure
  only while active; scope the diagnostic on the other checkpoint stops.
- Show the stall streak in the web Goal status strip, add a tooltip to the
  dialog row, and move the dialog copy away from "last check".
- Pin the SDK stall limit to core, the room-arm clear, the call-site cap,
  subclass classification and the retry hint in tests; restate the
  InvalidGoalCheckpointError contract and update the Goal docs.

* fix(goal): read a claim-count overrun as capacity and align checkpoint health surfaces

- describeCheckpointFailure reads GoalCheckpointClaimCountError as
  capacity, now that the count error has landed on main.
- goalCheckpointHealthVisible also shows the failure that stopped a Goal
  whose checkpoint request was too large; the Goals dialog copy follows,
  and gates on the raw diagnostic as core does.
- Headless /goal text output prints the checkpoint line.
- The request-too-large error names the measured size.
- Docs (get_goal description, record field, SDK type, goals.md, strip
  comment) state when the diagnostic is cleared and where the streak shows.
- The strip's stall pill shrinks on narrow panes instead of overlapping
  the actions.
- Tests pin the bare streak, the default clear on other checkpoint stops,
  every capacity subclass, the active stall-free summary, the stopped
  strip streak, and the dialog's complete and control-only cases.

* fix(goal): word checkpoint health once and pin the web shell's copy of its rule

- goalCheckpointHealthLine in core builds the checkpoint line the Ink card,
  the OpenTUI card and headless /goal text print, with an optional cleaner
  for callers that write straight to a terminal.
- The Web Shell's copy of goalCheckpointHealthVisible moves into goalGate,
  and a drift test runs core's own function body against it over every
  combination of the fields it reads.
- Headless /goal text and the Ink card sanitize the stop reason, which can
  embed a raw provider error.
- The status strip's stall label doubles as its tooltip, since it is
  ellipsized on narrow panes.
- The record field, SDK type and get_goal description say the diagnostic
  can also be the full-claim-list phrase; goals.md scopes the card claim to
  terminal cards.
2026-09-11 20:46:26 +00:00
qwen-code-dev-bot
18abbc8cb4
Merge branch 'main' into codex/model-api-selection 2026-09-12 03:27:54 +08:00
qqqys
28df8b8a78
feat(core): add permission_mode, agent_id and prompt_id to every hook input (#11618)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Lint & Static (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / web-shell E2E Smoke (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (no-AK, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (CLI, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Desktop Shell (ubuntu-22.04) (push) Blocked by required conditions
Qwen Code CI / Desktop Shell (windows-2022) (push) Blocked by required conditions
E2E Tests / Build for E2E (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 1/2 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 2/2 (push) Blocked by required conditions
E2E Tests / E2E Interactive - OpenTUI renderer (bun) (push) Blocked by required conditions
E2E Tests / channel-plugin E2E (nightly) (push) Blocked by required conditions
E2E Tests / cron-interactive E2E (nightly) (push) Blocked by required conditions
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
* feat(core): add permission_mode, agent_id and prompt_id to every hook input

Only tool, subagent and session-start events carried permission_mode, and
agent_id only appeared on SubagentStart and SubagentStop. Scripts shared
across events could not branch on either.

The common hook input now includes permission_mode (the session approval
mode, unless the event reports its own), agent_id when the event fires
inside a subagent, and prompt_id when the model turn is known.
PostToolUse and PostToolUseFailure also report duration_ms, measured from
the start of tool execution in both the core scheduler and ACP sessions.

The three copies of the approval-mode to permission-mode mapping are
replaced by one shared helper.

Part of #11610

* fix(core): time hook duration_ms on the monotonic clock

duration_ms on PostToolUse and PostToolUseFailure was the difference of two
Date.now() readings, so a system clock step while a long tool ran could
report a negative or inflated duration to audit hooks. Both the core
scheduler and the ACP session now read performance.now() and round the
elapsed time. The hooks guide also lists SessionStart among the events that
report agent_type.
2026-09-11 17:09:44 +00:00
qwen-code-dev-bot
181fd2ae7e
Merge branch 'main' into codex/model-api-selection 2026-09-11 22:46:55 +08:00
Dragon
91a09e76ae
fix(core): preserve Responses retry metadata and assistant phases (#11652) 2026-09-11 12:46:11 +00:00
ytahdn
9969640815
feat(web-shell): Edit and resend the latest user message (#11650)
Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
2026-09-11 12:06:38 +00:00
Shaojin Wen
20ecdaf6b2
feat(web-shell): add web previews with saved delivery history (#11276)
Some checks are pending
Qwen Code CI / Integration Tests (no-AK, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (CLI, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Desktop Shell (ubuntu-22.04) (push) Blocked by required conditions
Qwen Code CI / Desktop Shell (windows-2022) (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/1 (push) Blocked by required conditions
E2E Tests / cron-interactive E2E (nightly) (push) Blocked by required conditions
E2E Tests / web-shell Browser Regression (push) Waiting to run
E2E Tests / Build for E2E (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 1/2 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 2/2 (push) Blocked by required conditions
E2E Tests / E2E Interactive - OpenTUI renderer (bun) (push) Blocked by required conditions
E2E Tests / channel-plugin E2E (nightly) (push) Blocked by required conditions
npm cache producer / Save npm cache (push) Waiting to run
pnpm Worktree Smoke / Install (macos-latest) (push) Waiting to run
pnpm Worktree Smoke / Install (ubuntu-latest) (push) Waiting to run
pnpm Worktree Smoke / Install (windows-latest) (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
Update ECS Runner Qwen / Resolve version (push) Waiting to run
Update ECS Runner Qwen / Update Qwen on ecs-update-hk-1 (push) Blocked by required conditions
Update ECS Runner Qwen / Update Qwen on ecs-update-hk-2 (push) Blocked by required conditions
Update ECS Runner Qwen / Update Qwen on ecs-update-hk-3 (push) Blocked by required conditions
Update ECS Runner Qwen / Update Qwen on ecs-update-hk-4 (push) Blocked by required conditions
Update ECS Runner Qwen / Update Qwen on ecs-update-hk-5 (push) Blocked by required conditions
Update ECS Runner Qwen / Report a stale fleet (push) Blocked by required conditions
* feat(web-shell): add web previews with saved delivery history

* fix(serve): register saved preview telemetry route

* test(cli): update telemetry route drift count

* fix(web-shell): reclaim snapshot files and address preview findings (#11276)

* fix(web-shell): Replace undefined mock in split rerender tests (#11404) (#11406)

* fix(web-shell): Replace undefined mock in split rerender tests (#11404)

The two "does not rerender App for other split sessions" tests referenced
mockUseDaemonActivePromptBridge, which is not defined anywhere in
App.test.tsx — a deterministic ReferenceError that failed the main-branch
CI Test job at 70cf363395. The intended render spy is
mockUseDaemonSessionActivityBridge: App calls useDaemonSessionActivityBridge
on every render, and this file already uses that mock as the render-call
witness elsewhere.

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

* test(web-shell): Clear spy before rerender so the guard can fail (#11406)

The "does not rerender App for other split sessions" tests asserted
mockUseDaemonSessionActivityBridge toHaveBeenCalled() after rerender(),
but nothing cleared the spy since the top-level beforeEach mockReset(),
so it already held setup-time calls from renderApp()/flush()/the split
click and the assertion passed whether or not the rerender rendered App.
Clearing it immediately before rerender() makes the guard measure that
render only; the trailing mockClear() stays to isolate the loop below.

Mutation-verified: with the clear in place, deleting rerender() or the
useDaemonSessionActivityBridge() call in App.tsx turns both
parameterized cases red at the guard (both previously stayed green).

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

---------

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

* test(cli): complete worktree ACP config fixture

* fix(acp-bridge): keep restored saved-webpage records when snapshot retain fails (#11276)

* test(cli): make dangling-include screen test host-independent (#11276)

The test asserts runOneMutant's detail names vitest, which only holds
when findVitestBin throws "not found" from the bare fixture — but Node
resolution walks up from the probe tree, so a node_modules above the
runner's TMPDIR (observed on self-hosted CI, where jobs share one /tmp)
resolves a vitest, the probe then runs for real, and the detail carries
no 'vitest'. Plant a shadow vitest that declares no bin in the fixture:
the innermost node_modules wins resolution on every host, so the throw
is deterministic. Same pattern as the runControlMutant shadow in this
file.

* test(web-shell): align branding fixtures with workspace and sidebar behavior

* fix(acp): preserve non-strict ingestion on snapshot reference errors

---------

Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-09-11 10:01:34 +00:00
qqqys
518f6795f9
feat(workflows): move the authoring reference into a bundled skill (#11520)
* feat(workflows): move the authoring reference into a bundled skill

The Workflow tool's description carried the whole authoring contract —
orchestration policy, every agent() option, the error strings a script
compares against — about 17k characters across the description and the
`script` parameter, in the tool definition of every single turn. Only the
turn that actually writes a script needs any of it.

That reference is now the bundled `workflow-authoring` skill, and the tool
surface keeps what a model needs in order to decide whether to call the
tool at all and to read back a result: the opt-in rule, the limits, the
null-versus-throw contract, the run handle. Together they are 4.6k
characters, and the description ends with a line naming the skill.

A build the skill cannot reach is the case that makes this safe rather
than lossy: with the Skill tool denied, skills off, or this skill
disabled, the description inlines the reference instead, and falls back to
the pointer if the file itself cannot be read. The `workflow` keyword
already steers a turn toward orchestration, so that turn now carries the
reference in the same system-reminder — in the form the Skill tool would
have produced, and registered as loaded, so invoking the skill afterwards
dedups instead of sending it twice. A run that failed says which of the
two to go and read.

Part of #11013

* fix(workflows): name the authoring reference instead of injecting it

The `workflow` keyword put the whole 15 KB reference into the user's own
message text. That text is rendered in the transcript as what the user
typed, restored into the input buffer when a queued turn is cancelled,
and kept in history — and a body travelling that way got none of what a
real Skill load gets: dedup on resume, `/context` attribution,
microcompaction, the skill's declared side effects. Marking it loaded at
build time also let a discarded turn lock the skill out for the session.
The reminder now names the skill; the model loads it through the tool.

How the reference reaches the model is decided once, when the Workflow
tool is built, and recorded on it. A user who disabled the skill or the
bundled level gets neither a pointer nor an inlined copy, instead of the
reference moving into every request. A Skill tool deferred by
`tools.eager` gets a pointer that names the ToolSearch detour, or the
inline shape when nothing can reveal it. The `script` parameter, the
failure hint and the keyword reminder all read the recorded shape, so a
mid-session `/skills` toggle cannot make them contradict the description.

The inline shape no longer repeats the runtime paragraph the reference
already states. The failure hint reaches compile failures and failed
background runs, and is left off saved workflows and cancelled runs. The
reminder is skipped in shell mode and when the Workflow tool is absent.

The reference now states what the runtime does: all of `Date` throws,
an eager `parallel()` is rejected after its agents were spent, a null
pipeline stage skips the rest of the chain, the `phase` option is
ambient, all six disallowed tools, `workflow({ scriptPath })`, the four
clamped overrides, and every journal line type. Its worked example
handles each null in the stage that dispatched it and gives every
verifier its own label, and a test runs it.

Part of #11013

* fix(workflows): make every surface agree on how the reference is reached

A deferred tool is not the same as a hidden one. The Skill tool counted
as "reveal it with ToolSearch first" even when `tools.visible` declared
it or a reveal had already happened, and the keyword reminder steered
toward a Workflow tool whose schema was in no request. Both now ask
whether the schema is actually withheld, through one core helper that
mirrors the registry's own check without its factory-only false
negative. The ToolSearch sentence is conditional, because the
description is built once and a reveal can land later, and it is one
shared wording: the description, the failure hint and the reminder all
use it, where the hint previously dropped it for a deferred Skill tool.

The reference now states what the runtime does in the places it did
not: the concurrency window follows `availableParallelism()`, every
worktree isolation refusal (and `workingDir` for the nested case), and
a bad `workflow()` name ends the run only at the top level. The worked
example refuses to run without its input, logs a dimension a stage
dropped, and reports refuted findings next to confirmed ones.

Switches over the route and the surface keep a `never`-typed default,
so a new member is a compile error rather than a silent pointer. The
design docs state the measured baseline instead of source-text sizes.

New tests pin the shell-mode gate at the AppContainer call site, the
hint travelling through the runner into a background notification, no
hint for a saved workflow's session-owned run, the hint on a generated
copy re-run by path, the hint on the withheld and deferred shapes, the
skill level passed to `isSkillEnabled`, and a ceiling for the inline
description.

Part of #11013

* fix(workflow): address round-3 review of the authoring skill

- Route through ToolSearch for a deferred Skill tool even when it is revealed
  at build time: the surface is recorded once and /clear drops reveals.
- Drop the failure hint wherever the runner resolves a saved-workflow name,
  including a retry that re-runs the inline source without passing the name.
- Return refuted findings from the worked example, not only their count.
- Pin the disallowed-tool sentence to the orchestrator's list, the refute
  anchor to its payload, and state the 30-second synchronous vm cap from an
  exported constant.
- Exercise the keyword reminder's record-over-live-opt-out direction.
- Scope the shell-mode claim to the keyword reminder at submit time; the
  older gaps are tracked in #11626.

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-09-11 08:56:25 +00:00
ChiGao
7156037d6a
fix(cli): close the OpenTUI renderer's parity gaps against ink (#11630)
* fix(cli): give the OpenTUI confirmation dialog ink's per-type approvals

The OpenTUI dialog offered one generic four-row list for every tool
confirmation. That cost three things users rely on when deciding whether
to grant access.

The always-allow rows did not say what they were allowing, so approving
"Always allow in this project" gave no hint that the grant covers every
`touch` invocation rather than the one command on screen. They were also
offered in an untrusted folder, where a durable permission rule for a
workspace the user has not trusted is not a decision the dialog should
put in front of them.

Plan and edit confirmations got the wrong choices outright: a plan was
never offered the chance to restore the approval mode it replaced, and an
edit was offered the project/user-scoped persistence outcomes instead of
the session-wide allow-always that an edit actually supports.

Approval rows are numbered again, and a digit key picks one, matching the
inline prompt. Each type now also shows its own question line, so an
execution approval names the command root being permitted.

* fix(cli): warm the shell AST parser before the OpenTUI renderer boots

The OpenTUI renderer constructor installs a bare `globalThis.window` to
hang its requestAnimationFrame shim on. web-tree-sitter's UMD wrapper
probes `window.document.currentScript` when it is first evaluated, so
the first dynamic import after that point throws, and the parser latches
that failure permanently for the rest of the process.

Every OpenTUI session therefore silently lost AST-based shell analysis:
permission rules came back empty, so always-allow confirmations lost
their scope and persisted nothing, and read-only detection and
command-safety classification dropped to their conservative fallbacks.

Initialise the parser before the renderer exists, while `window` is
still undefined. Adds a one-line re-export from the core package index;
web-tree-sitter stays dynamically imported, so the deferred-runtime
invariant is unchanged.

* fix(cli): give the OpenTUI footer ink's status rows and token estimate

The status row was truncated to a single line, so a narrow terminal dropped
the model segment that ink wraps onto a second row. The hint row now carries
the approval-mode name the composer stopped drawing, since ink uses that text
as an aria-label rather than a visible row and this renderer has no aria
surface. The hint row is still truncated, so it cannot grow the footer
mid-turn.

The loading indicator advertised an output-token estimate it never received:
the character counter and the receiving flag were declared as props but no
caller set them, so the estimate stayed pinned at zero and the direction arrow
always pointed down. Both now come from the live turn, counting model text,
thoughts and tool-call arguments the way ink does, and flipping back to the
waiting phase when tool results go to the model.

The footer also hid while a completion list was open only in ink; here it
drew under the dropdown and pushed the composer around.

* fix(cli): match ink's completion dropdown and bind Shift+Tab in OpenTUI

The slash dropdown never populated a command's source badge, so skill, plugin
and MCP rows lost the marker ink draws and the shared label column was sized
without counting it. Descriptions were emitted verbatim, so a multi-line
command blurb kept its frontmatter line breaks and an over-wide one spilled
onto a second row, doubling the height of the popup. The row budget also
ignored that the dropdown nests its own side margins inside the composer's,
which left two columns too many for the description and wrapped its tail.

A label and its argument hint were concatenated into one run, so a hint too
long for the column word-wrapped the whole string and grew the row to three
lines where ink produces two. They are now laid out as separate children and
broken at the column edge, which puts the continuation at the hint's own
offset exactly as ink does.

Shift+Tab was unbound, so the footer advertised a cycle shortcut that did
nothing and the mode could only be changed through the dialog. The shell now
holds the mode the way ink's indicator does: it cycles on the keystroke,
adopts the dialog's choice into the same state, reports a refused change
instead of repainting a mode it does not hold, and explains an entry into auto
mode — including at startup, where no keystroke ever runs. Windows accepts a
bare Tab because some terminals there cannot distinguish the two, and the
completion consumers both claim the key first, so a Tab spent accepting a
suggestion never reaches the cycle.

The cycle order now comes from the shared list of modes rather than a copy of
the enum's declaration order, so it cannot drift from the one ink walks.

* fix(cli): keep the OpenTUI auto-mode notice from reprinting on a re-pick

Both of ink's routes into auto mode gate the entry notices on the session not
already holding it. The rotation could never violate that, so the guard looked
redundant here and was left out — but the approval-mode dialog opens with the
current mode already selected, so a bare Enter re-picks it. The first-time
message survives that because it is acknowledged in settings; the notice listing
the allow rules auto mode stripped does not, and would reprint on every re-pick.

The gate now sits where both routes meet, and the comment that claimed ink's
slash command had no such gate is corrected: it does.

* fix(cli): give OpenTUI @-mention rows ink's full-width label column

The dropdown sized its label column the same way whatever was being completed.
That is right for slash commands, where every row shares one half-width command
column so the descriptions line up, but a file list is not a command list: ink
only shares a column there when a row carries a description to line up against,
and lets a plain path take the whole row. Clamping it to half the width instead
wrapped any path longer than that onto a second row, mid-word, so one entry
occupied two lines and pushed the rest of the list down.

The column now follows the mode being completed, and the cell keeps the shared
width only where a description has to line up against it. The minimum readable
description width that caps the shared column moves next to the other constants
the two renderers have to agree on, rather than being copied out of one of them.

* refactor(cli): drop the OpenTUI dropdown's unreachable described-row column

The label column gained a branch for rows that carry a description, mirroring
the other renderer. That branch cannot fire here: this renderer's `@` completion
only ever asks the file index, and its results carry no description, so the only
rows that reach the dropdown with one are slash-command rows — which already take
the shared half-width column. The column is therefore either that column or none,
and the minimum readable description width that capped the dead branch goes back
to being local to the renderer that uses it.

* fix(cli): match ink's popup geometry and its help and model dialogs

Every popup this shell renders spanned the terminal edge to edge, while ink
wraps its popups in a two-column margin and caps their width, so a border ran
from column 2 to column 97 and stopped there. The wrapper now supplies both,
which is what makes a dialog read as a dialog rather than a full-screen mode.
The confirmations stay outside it: their body measures the terminal width to
estimate how its text wraps, so narrowing the box without also narrowing that
measurement would corrupt the estimate.

The help dialog opened on its command list rather than its overview, and its
tab keys did not match the ones the footer advertises. Both now follow ink:
the overview is the opening tab, Tab and Shift+Tab cycle it in the two
directions the hint promises, the arrow and page keys move the command window
and are inert on a tab that has none, and no other key closes or navigates.
Closing on a bare letter key meant a typo dismissed the dialog.

That command window was a fixed eighteen rows whatever the terminal height,
which is more than the body budget leaves once the tab's own introduction line,
its gap and the scroll hint are counted. The overflow was resolved by dropping
the gap under the introduction and clipping the hint away entirely, so the
scroll position the hint reports was simply absent. The window is now sized to
what the budget leaves after that chrome, and paging moves by the window that
is actually on screen. Below a 42-row terminal this shows a shorter list than
ink does, which is the smaller loss: ink keeps its eighteen rows and clips the
hint instead.

The model dialog drew its detail rule twenty characters wide against ink's
full-width one, and showed no line under a model's title. ink folds the runtime
and discontinued markers into the row description as well as the title, so a
runtime model with nothing of its own to say still gets an explanatory line;
the entries now carry that, and the rule is spelled out to the frame's inner
width because there is no single-sided border here to draw one with.

* fix(cli): align the OpenTUI transcript, footer and dialogs with ink

A frame-by-frame comparison of both renderers over 14 pty scenarios
turned up a set of divergences that were invisible in unit tests. This
groups them by topic.

Tool output is a snapshot, not an increment. Core hands the shell the
whole accumulated display on every progress event, so a running tool
card that appended painted the streamed text once per event. The event
field is renamed to say so, the card replaces instead of accumulates,
and the throttle that stops a redundant re-emit now compares lengths.
Shell mode loses its tail-deduplication tail and writes the same string
to the card and to the model history, so the two cannot disagree.

The transcript and the composer get ink's horizontal margins, which
makes the dropdown, the attachments and the body columns land where ink
puts them. The status rows gain the prefixes ink draws -- a warning row
had none at all -- and the error row collapses its hint onto the same
line instead of a second one, with the prefix pinned so a wrapped body
cannot shrink it away.

The footer's loading indicator moves to ink's indent, and an armed quit
warning now renders even while a dialog, a confirmation or a dropdown
has the rest of the footer hidden. Previously the gate that hid the
footer also hid the warning, so pressing Ctrl+C once with a dialog open
gave no feedback at all.

Shift+Tab cycles the approval mode from the top level, so it keeps
working while a dialog or a confirmation has the composer unmounted --
the on-screen hint advertises it in exactly those states. The Windows
fallback that also accepts a bare Tab is narrowed to bare Tab only,
since a terminal that can tell them apart would otherwise cycle twice.

The context-files announcement is latched with the predicate ink uses
and re-armed when the transcript resets, so a new session in the same
renderer announces its context files again.

The model dialog carries the three guards ink has. A successful pick no
longer also announces the model that survived, and neither a second
Escape nor a second Enter landing mid-apply can announce or start a
second switch.

Dialog selection markers use ink's chevron rather than a filled bullet,
and the auth dialog takes the shared icon glyphs.

Adds the bilingual design doc recording each decision, the coverage
boundary and the follow-ups.

* fix(cli): give OpenTUI transcript items ink's per-type top margin

ink leaves a blank row above most conversation items and decides it per
item type in its history renderer; this renderer printed every item
flush against the one before it, so a completed turn read as one
unbroken block where ink reads as separated rows.

The comparison harness had been folding the gap away: it reduces each
captured frame to its non-blank rows, so a capture could be reported
byte-identical while the two renderers disagreed on every vertical gap
between items. Measuring the transcript region alone, as a run-length
pattern of blank and content rows, isolates it and is unaffected by the
two renderers anchoring to opposite ends of the screen. After the fix
that pattern matches ink's group for group in every capture that
carries a conversation item.

Each item is now wrapped in a box carrying the margin resolved from its
kind. Two kinds have no ink counterpart -- a task card, because ink
renders a subagent as the tool that spawned it, and an image row,
because ink draws images inline inside the message that carries them --
and both take zero so they stay flush against the tool row beside them.
The margin sits on the wrapper while the per-item row cap applies to the
item's own content, so the two are additive and neither can clip the
other.

The design documents record the mapping in both languages, along with
two limits of the evidence: layout props are unobservable in the unit
test runtime, so this is verified by frames only, and the blank rows ink
shows around its banner come from how it batches permanent writes rather
than from a layout rule, so they are deliberately not reproduced.

* fix(cli): close four OpenTUI gaps against ink's approval, thought and tool-result paths

An approval-mode switch now confirms the calls it would not have parked.
ink pairs the switch with the release; the selection rule deciding which
calls qualify was already ported here but had no caller, so the renderer
had the rule and not the behaviour.

A committed thought names its duration the way ink does, and the key its
collapsed hint advertises is now bound at the entry, so it still lands
while a dialog or a confirmation owns the screen.

A tool result's structured payload is resolved in one place. Five of the
six paths that turn a result display into events flattened straight to
text, which put a todo list's raw JSON on screen, on resume and on
replay, while the card's own checkbox-list renderer sat unused.

* docs(cli): retract the OpenTUI OAuth gap and keep the MCP approval one

The follow-up list reported two of ink's authentication progress screens as
missing here, on the grounds that a login could not be completed without
them. Neither branch is reachable in ink: no provider in the registry
declares that auth type, so the single write site for the pending auth type
cannot produce it, and the external-auth state those screens read is
assigned null at both of its write sites. Record the check so the omission
is not re-reported as a gap.

The MCP server startup approval in the same bullet did survive: ink opens
that dialog whenever its approval queue is non-empty and this renderer has
no counterpart, so it stays.

* fix(cli): ask for approval of a project's gated MCP servers in OpenTUI

A server list checked into a project was never offered for approval in
this renderer, so a gated server stayed silently disconnected and
nothing told the user why or what to do about it. ink opens the dialog
whenever its approval queue is non-empty.

The queue, the decision that persists against a hash of the config, the
un-gating and the reconnect all live in a renderer-agnostic hook, so
this is a view and a mount point rather than a second copy of the
policy. It ranks above the shell and tool confirmations, as in ink.

Geometry was measured against ink at a hundred columns rather than
assumed, and the first attempt was wrong twice: inset three with a box
one narrower than the shared popup width, and numbered rows. Declining
lands on the same composer and footer row in both legs. Two residuals
are recorded instead of matched — ink's right border is clipped away by
its own overflowing margin, and one wrapped body line carries an extra
leading space here.
2026-09-11 08:43:02 +00:00
qqqys
c541bdffa4
fix(core): route plain-text hook stdout to additionalContext for prompt and session events (#11612)
* fix(core): route plain-text hook stdout to additionalContext for prompt and session events

A command hook that prints plain text and exits 0 was turned into a
systemMessage, which the SessionStart, UserPromptSubmit and
UserPromptExpansion consumers never read and nothing displays. An
`echo "context"` hook on those events was therefore a silent no-op.

On those three events plain-text stdout now becomes
hookSpecificOutput.additionalContext, so it reaches the model and chains
into the next sequential hook like JSON context does. Other events keep
the systemMessage conversion. Only stdout is promoted: the stderr fallback
used when stdout is empty stays a system message, and non-zero exits keep
their existing handling.

Part of #11610

* fix(core): harden plain-text hook stdout promotion

Review follow-up. Only a JSON object (optionally double-encoded) is
structured hook output; bare JSON values such as `42` or `[1, 2]` are
plain text instead of being dropped, and exit code 2 with a bare value on
stderr now blocks. Output shaped like a JSON object that fails to parse is
kept out of model context, as in Claude Code, with a debug-log warning.
Promoted text has terminal escapes and control characters stripped per
line. The comments now state the real rationale for the event set and
the actual stderr behaviour, and the hooks guide row matches.

Part of #11610
2026-09-11 08:09:21 +00:00
qqqys
5cefd97ba3
fix(goal): strip fences and retry the checkpoint verifier on the shapes a note can fix (#11578)
* fix(goal): strip fences and retry the checkpoint verifier on the shapes a note can fix

A checkpoint reply wrapped in a markdown fence, carrying more than 32
claims, citing ids outside the request, or changing a source's proof kind
failed the check on the first attempt, and on an overflowing window each
failure spent a stall that one corrective call could have saved.

- Parse a reply wrapped in a single markdown fence without the fence.
- Report a claim count over the limit as GoalCheckpointClaimCountError.
- Check cited ids and proof kinds against the request inside the verifier,
  as GoalCheckpointSourceRefError and GoalCheckpointProofKindError, the
  same rules materializeGoalEvidenceCheckpoint applies after the call.
- Give all three the one corrective call the budget and length overruns
  already get, naming the measured violation; echoed model-written ids are
  capped. Invalid JSON and empty claims stay single-shot.

Refs #11577

* fix(goal): unwrap checkpoint fences in linear time and name every source violation

- Replace the whole-reply fence regex, which backtracked quadratically on an
  unclosed fence over a whitespace run and blocked the event loop past the
  verifier timeout, with index scans; recognise backtick and tilde fences of
  three or more characters, any info string, and one-line fences.
- Collect every proof-kind mismatch before throwing and name them all in the
  corrective note, including alongside unknown ids.
- Strip control characters from model-written ids before echoing them into
  error messages and notes, and say how many ids were left out.
- Name the once-per-claim and per-claim sourceRefs rules in the notes that
  advise merging.
- Pin the verifier's source rules to materializeGoalEvidenceCheckpoint.
- Point the goalCheckpointTimeoutSeconds description at the Goals doc instead
  of restating the retry triggers, and regenerate the settings schema.

Refs #11577

* fix(core): state sourceRefs bounds up front and quote echoed ids in retry notes

- The system prompt and the budget note now state that a claim lists each
  id once and cites at most GOAL_CHECKPOINT_SOURCE_REFERENCE_LIMIT ids; both
  bounds are stripped from the emitted schema and a breach is single-shot.
- Retry notes quote every model-written id and label the quoted ids as
  untrusted data.
- GoalCheckpointProofKindError keeps `mismatches` as its only
  representation; GoalCheckpointSourceRefError takes its mismatches
  explicitly.
- Pin the backtick info-line fence rule both ways and the proof-kind
  mismatch cap in the note.
- goals.md lists the rejections that get no corrective call.
2026-09-11 07:43:40 +00:00
Dragon
4ec9f63d0a
feat(core): add CodeModeOnly tool execution (#10607)
* feat(core): add CodeModeOnly tool execution

* fix(core): preserve CodeModeOnly execution semantics

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

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

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

* chore: refresh settings schema

* chore: refresh companion notices

* fix: register exec tool metadata

* fix(cli): localize exec tool display name

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

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

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

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

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

* test: include code mode host in standalone fixture

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

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

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

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

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

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

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

* fix(cli): pin patched Ink version

* feat(cli): add collapsible tool call details

* fix(dev): use CLI tsconfig outside repository

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

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

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

* feat(core): add code mode timeout helpers

* feat(cli): expose code mode in settings

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

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

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

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

* fix(core): recover from rejected Responses encrypted reasoning
2026-09-11 07:40:35 +00:00
Shaojin Wen
642d36e6d8
feat(subagents): add Codex executor and native agent builtins (#11474)
* feat(subagents): add Codex executor and native agent builtins

* fix(subagents): enforce native grants and retain Codex results

* fix(subagents): preserve native outcomes and parent permissions
2026-09-11 04:44:23 +00:00
ytahdn
53e6196c6b
fix(web-shell): persist cancelled responses and elapsed time (#11608)
* fix(web-shell): preserve cancelled responses and elapsed time

* fix(core): retain cancelled text during reactive compression

---------

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
2026-09-11 04:30:09 +00:00
Shaojin Wen
d66ad3e3a2
feat(review): focus automatic static navigation reviews (#11456)
* feat(review): focus automatic static navigation reviews

* fix(review): address round-1 review of the docs-nav profile (#11456)

- QWEN_REVIEW_AUTOMATIC is operator-only: added to
  PROJECT_ENV_HARDCODED_EXCLUSIONS and read through
  automaticReviewRequested(), which refuses file-sourced values
- SKILL.md: the docs-nav profile passes the cumulative findings file to
  compose-review as findingsPath, so an unruled candidate caps at
  Comment instead of posting as a verified blocker
- the classifier treats top-level key order as structure (a reorder
  keeps the full review); the design doc records the decision
- assessResume reports a moved head as head-moved even on a profiled
  report; both profile guards read the shared DOCS_NAV_PROFILE constant
- the mechanism-health note no longer reads the profile's by-design
  anchor withhold as a stopped chain
- the coverage gap subject names the omitted full review and reverse
  audit instead of the review that ran
- buildRoleBrief welds no context pointer for a malformed plan prNumber

* fix(review): address round-2 review of the docs-nav profile (#11456)

* fix(review): address round-3 review of the docs-nav profile (#11456)

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

* fix(review): close focused navigation review gaps

* fix(review): strengthen focused navigation contracts

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-11 03:59:55 +00:00
Xiong Wang
f649d65d1f
feat(live): add visual input, proactive assistance and memory (#11369)
* feat(live): add visual input, proactive memory and desktop controls

* fix(live): preserve upstream checks and finalize migration validation

* feat(live): open active configuration from host settings

* fix(live): address lifecycle and proactive review findings

* feat(live): add subagent controls and diagnostics with review fixes

---------

Co-authored-by: 菲兹 <feizi.wx@alibaba-inc.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-09-11 03:13:13 +00:00
Shaojin Wen
c6c276b05c
feat(web-shell): improve composer and context usage presentation (#11509)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Lint & Static (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / web-shell E2E Smoke (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (no-AK, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (CLI, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Desktop Shell (ubuntu-22.04) (push) Blocked by required conditions
Qwen Code CI / Desktop Shell (windows-2022) (push) Blocked by required conditions
E2E Tests / Build for E2E (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 1/2 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 2/2 (push) Blocked by required conditions
E2E Tests / E2E Interactive - OpenTUI renderer (bun) (push) Blocked by required conditions
E2E Tests / channel-plugin E2E (nightly) (push) Blocked by required conditions
E2E Tests / cron-interactive E2E (nightly) (push) Blocked by required conditions
E2E Tests / web-shell Browser Regression (push) Waiting to run
pnpm Worktree Smoke / Install (macos-latest) (push) Waiting to run
pnpm Worktree Smoke / Install (ubuntu-latest) (push) Waiting to run
pnpm Worktree Smoke / Install (windows-latest) (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
* feat(web-shell): improve composer and context usage presentation

* fix(web-shell): address context presentation review feedback

* test(web-shell): align brand fixtures with startup and layout

* fix(web-shell): improve context readability and availability
2026-09-11 03:01:33 +00:00
jinye
2f426a64f4
feat(web-shell): enrich browser notifications and open target sessions (#11447)
* feat(web-shell): enrich browser notifications and open target sessions

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

* feat(web-shell): include current prompt in browser notifications

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

* feat(web-shell): configure browser notification branding and defaults

Allow hosts to configure notification names, icons, and the initial preference. Enable notifications by default in the built-in page while preserving saved user choices and browser permission requirements.

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

* fix(web-shell): address browser notification review feedback

Preserve code excerpts, bound pending notification text, and keep notification configuration changes from restarting session streams. Reconcile branding documentation and add regression coverage.

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

* fix(web-shell): address notification follow-up review cases

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-09-11 02:27:39 +00:00
qqqys
5a9c9b3406
feat(goal): stop a Goal at a turn or an active-time budget (#11457)
* feat(goal): stop a Goal at a turn or an active-time budget

The token budget is a runaway-spend guard, sized for that job: 30,000,000
tokens by default, which a healthy long run reaches late. It is not what a
user reaches for to keep a Goal short -- people say "at most twenty turns"
or "at most half an hour". Neither had any effect, and the objective
template invites the first phrasing while the user doc has to explain that
writing it configures nothing.

Add two ceilings beside the token one, both off by default:
`model.goalMaxTurns` counts finished Goal turns and `model.goalMaxActiveMinutes`
counts the wall time the Goal spends active. The meters were already on the
record; only the ceilings and the stop were missing.

A Goal that reaches either takes the path a spent token budget already
takes: one wind-down turn to hand off, then `usage_limited` with a new
`limitKind` (`turn_budget` or `time_budget`) and a reason naming the budget.
A resume authorizes another window on top of what was used and moves only
the ceiling that ran out. Ceilings are read at the continuation boundary, so
the turn that crosses one still finishes; a turn crossing several reports one
reason, token first.

- `spentBudget` replaces the runtime's direct token check, so the
  continuation gate, the settle, and the no-progress bound cannot disagree
  about whether a Goal is out of allowance. The no-progress pause yields to
  every spent budget, not just the token one, so a cadence stop still gets
  its hand-off.
- The reducer's budget-resume branch keyed off the literal `token_budget`;
  it now keys off `isGoalBudgetLimitKind`, or a turn-stopped Goal would
  resume still rendering "ran its turn budget" as the reason it is active.
- The wind-down line stops naming the token budget, since the hosts carry a
  plain `windDown` flag and cannot say which ceiling was reached. It points
  at the budget line above it, which grew to report turns against their
  ceiling and, when one is armed, active minutes against theirs.
- Both new `limitKind` values reach the two whitelists outside core: the
  SDK's hand-copied union and the Web Shell's `getGoalState`, which rebuilds
  the record field by field and would otherwise drop them on the live path.

Design: docs/design/2026-09-09-goal-turn-and-time-budget.md and its
.zh-CN.md counterpart.

* fix(goal): repair the assertions and mocks the budget change broke

Six CLI assertions pinned the continuation prompt verbatim, and renaming
the budget line's prefix and rewording the wind-down line moved that text
out from under them. Two more CLI suites could not collect at all: they
build their `@qwen-code/qwen-code-core` mock by enumerating exports, and
`settingsSchema.ts` now reads two new caps at module scope.

Neither was reachable from the verification steps this change shipped
with, which ran only `runBudget.test.ts` and `config.test.ts` in the CLI
package. `cd packages/cli && npx vitest run` reaches all eight.

Also, while in these files:

- `get_goal`'s `lastGoal` summary already reported `tokenBudget`, so a
  Goal stopped by a cadence ceiling could be inspected and show only the
  allowance that did not run out. It now reports all three, and the tool
  description says so.
- Two validator doc comments said "-1 for no ceiling, or a positive
  integer"; the predicates accept `0` as the same opt-out, the way the
  token budget's sibling already documents it.
- `settingsSchema.test.ts` now pins both new entries: integer, no
  default, bounded by their caps, no restart.

* fix(config): reject signed zero for the Goal cadence settings, and say what the ceilings actually do

The `excludedValues` guard added last round compared with `Object.is`, which
tells `+0` from `-0`. `Number()` maps `-0`, `-0.0`, `-.0` and `-0e0` to `-0`,
`Number.isInteger(-0)` is true and `-0 < -1` is false, so `/config
model.goalMaxTurns=-0` walked past the guard, reported `Set ... = 0` because
`String(-0)` is `"0"`, and `JSON.stringify` persisted the excluded value.
Every later start in that scope then aborted on `must be > 0`, with `/config`
unreachable to repair it -- the same lockout the guard was added to prevent.
Compare numbers with `===` so signed zero matches, keeping identity for the
string half of the field's type.

The documentation of both ceilings overclaimed in three ways, each fixed
wherever the claim is published -- the two schema descriptions and their
generated mirror, the settings table, the user guide, both locales of the
Web Shell placeholder, three places in the goal-draft skill, and the
`propose_goal` objective example:

- `-1` was described as running Goals with no ceiling. It only removes a
  ceiling a Goal has already spent, on the resume or edit that follows; a Goal
  still under its ceiling keeps it.
- Neither ceiling is retrofitted onto a Goal already on the record, so the
  only way to bound one is to replace it. Nothing said so.
- The minutes row read as a wall-clock cap. There is no timer: the ceiling is
  read between turns, so a turn already running is never interrupted and the
  window can be exceeded by that turn plus the hand-off.

The design doc's "no behavioural change when off" bullet and its first
acceptance criterion were also too strong, in both languages. Restoring an
`active` Goal rebases `updatedAt` unconditionally, so the elapsed figure a
restored Goal reports drops offline time for everyone, settings or not -- and
drops with it any active time in an interrupted turn that no journal write had
committed. Both are now stated rather than implied, with the reason the record
cannot separate the two.

Two promises the tests did not hold:

- Three surfaces say user turns are still admitted at the ceiling, and the only
  case covering it admitted the turn one step below the limit. A case now
  reserves a user turn with the ceiling already spent and the hand-off in
  flight; adding a `spentBudget` gate to `beginTurn` reds it.
- The active-time ceiling is the only one whose spent state can change between
  queueing a continuation and delivering it. A case now queues under the
  ceiling with no host and binds past it, asserting the hand-off; making
  `bindHost` flush directly reds it.

* fix(goal): name what replacing a Goal costs, keep ceiling advice out of objective text

Round-three review, four findings.

Replacing a Goal is the only way to bound one already on the record, and the
docs offered it without the cost. `/goal set` goes through `createGoal`: a new
Goal at revision 1, turn, token and active-time meters at zero, and an
evidence cursor at the replacement, so everything the old Goal recorded drops
out of the citable catalog. Said so on every surface that offers the remedy --
the user guide, both settings rows and both schema descriptions, regenerated.

The ceiling settings were pointed at from two opposite places: the goal-draft
template slot said "outside the objective", while its exemplar, the user
guide's Budget row and strong example, the `propose_goal` objective example
and the Web Shell placeholder all wrote the pointer into objective text -- text
re-sent every turn to a model that cannot act on it, advising a setting that by
its own wording never applies to the Goal carrying it. One placement now:
never in objective text. The rules-of-thumb bullet names the settings, the
user guide's Budget row says to set them in settings, and the Web Shell pointer
moves from the placeholder into the hint rendered under the same field.
`SKILL.test.ts` pins both halves -- the settings are named in the skill body
and absent from the template and the exemplar -- so they cannot drift apart.
The strong example also gains the `On block:` clause the template asks for.

The signed-zero guard is one `includes` call: SameValueZero already matches
`-0` against a `[0]` exclusion, so the typeof-split ternary carried an arm no
setting can reach. The `-0` assertion from last round still pins it.

The settings table rows are trimmed to fit the widest existing row, so the
table is not re-padded, and one shortening that would have misdescribed which
values are rejected is reverted to the accurate list.

* fix(goal): stop the ceiling docs promising more than the runtime does

Round-four review, five findings, all documentation precision. Each one is
fixed by narrowing a claim rather than qualifying it further, so the surface
the next round can find fault with gets smaller.

- Bounding a Goal already on the record: "replacing it with /goal set or
  clearing it, which starts a new Goal" said clearing starts a Goal. Clearing
  leaves none. Only the replacement starts a new Goal at revision 1; clearing
  is followed by starting again. Both settings rows and both schema
  descriptions now say so, matching the user guide.
- "Time while the process is not running does not count" overclaimed. The
  only exclusion is the rebase a restart performs; a suspended process, such
  as a sleeping laptop, is charged. The claim now says exactly that.
- The quantified overshoot, "that turn plus the hand-off turn", holds only
  when a turn is running as the window runs out. A window that runs out while
  no turn runs is not noticed until the next turn ends. The bound is dropped
  in favour of saying the Goal can run well past the window.
- The Web Shell hint sent readers to settings for two keys that client's
  settings surface cannot reach. It now names `/config` in the chat, which
  runs in daemon sessions.
- The second weak-to-strong exemplar still wrote a bare turn count into its
  Budget. It carries the advisory marker like the first, and the skill test now
  checks every exemplar row instead of one.

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
2026-09-11 01:01:02 +00:00
qqqys
af4dece3a7
feat(sdk): add a peer endpoint so a program outside Qwen Code can join cross-session messaging (#11560)
* feat(sdk): add a peer endpoint so a program outside Qwen Code can join cross-session messaging

`@qwen-code/sdk/peer` is an implementation of the cross-session protocol
for a program that is not itself a Qwen Code session — a voice
front-end, a relay, a build watcher. `PeerEndpoint.start({ name })`
binds an inbox and publishes a session record, so the program is listed
by `qwen sessions ps` and by every session's `list_agents`; `list()` and
`send()` address sessions by name; `onMessage` receives what they send;
receipts are applied as state transitions and can be awaited.

It is written from the protocol page with Node's own modules alone and
shares no code with Qwen Code. That makes it the page's first
independent implementation, and a conformance suite runs it against
Qwen Code's own registry, inbox, directory and send path in both
directions: listing and reaching each other, frames parsed and answered
each way, a folded drop receipt settling a burst, a user-minted
controller token recognised and a forged one not, identical verdicts on
hostile lines, record files and names, and every published limit pinned
to Qwen Code's constants.

The endpoint takes the shared `<pid>.json` when it is free and a minted
`<pid>-<8 hex>.json` when it is held, never replacing a record it cannot
prove dead. Its inbox accepts only its own token, it never deletes
records or sockets other processes wrote, and trust comes only from a
controller token. On Linux it refuses to publish a record without a
start token and PID namespace.

Implementing the page found two rules it left implicit, now stated in
"Writing your own record": `procStart` and `pidNs` are required on
Linux, and a held `<pid>.json` means writing the minted name instead.

Separate Node-only subpath, kept out of the browser-bundled entries;
the build checks its size and that it carries no runtime dependency.

* fix(sdk): present the controller token only on sends marked controller

Review follow-ups on the peer endpoint.

The controller token was presented on every send, so any process that
published a record under a name the program then addressed read the
plaintext of a credential Qwen Code only ever stores hashed. A send now
presents it only when marked `controller: true`; unmarked sends — from an
endpoint holding a token too — present the recipient's own token. On a
marked send, two records for one session id and name are ambiguous rather
than collapsed to the newer, so a copied record cannot take the grant. A
malformed `controllerToken` (empty, no `qpc_` prefix) is rejected at start
with `invalid-controller-token`, as is a marked send without one.

Other fixes:
- Two `start()` calls in one process raced for `<pid>.json` and one
  record was lost; registrations inside a process now run one at a time.
- `send()` checks `closed` again after the directory read, so a send
  overlapping `close()` rejects instead of writing a dead reply address.
- `ambiguous.matches` holds only addresses that select one session.
- Receipt `reason` is flattened and bounded like every other label.
- The local concurrent-send ceiling is marked `local` and described as
  this program's limit, not as a busy peer.
- An explicit `socketPath` no longer re-permissions a directory the call
  did not create.
- `probePeerSocket` is renamed `probePeerSocketVerdict`, matching core;
  the status list is the single source of the status type.
- `flattenPeerLabel` matches control and format characters by Unicode
  category and caps in code points, in core as well, so the two
  implementations still agree.
- The build checks every emitted peer declaration, from a module with
  tests of its own; the surface test is compiled by the typecheck and
  matches imports as path segments.

Docs: `list_agents` and `send_message` need `agents.crossSessionMessaging`
on the receiving session; delivery without review also follows a matching
`fromMode`, and `agents.crossSessionInbound` outranks a controller grant;
a controller send to another endpoint is dropped unread; the endpoint's
inbox applies none of the protocol page's §6; the `socketPath` sibling
fallback; and the protocol page gives separate reasons for `pidNs` and
`procStart`. The design doc no longer says the default entry ships to
browsers.

Tests pin each of these, plus the paths the first round left unexercised:
ledger and answered-id eviction past their bounds, start rollback, the
Linux identity guard, registry directory tightening and read errors, the
over-long line with the default deadline, the symlinked and over-long
socket paths, prompt close, Windows, and a live PID with a mismatched
start token.
2026-09-10 22:13:39 +00:00
qqqys
4815941476
feat(goal): start approved Web Shell proposals after their owning turn (#11360)
* feat(goal): start approved Web Shell proposals after their owning turn

* fix(goal): close proposal lifecycle gaps

* fix(goal): address settlement review gaps

* fix(goal): block proposal settlement when a guard continuation yields to a queued prompt (#11360)

#runStopContinuation's skipped-send funnel returned a plain end_turn for
the queued-prompt and unreliable-drain stops without marking the turn's
settlementBlocked flag, so a parked proposal approved earlier in the turn
was applied even though the turn ceded to the queued user prompt. Set the
flag at those three stops, mirroring the loop-level exits, and pin each
with a regression that fails without it.

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

* test(cli): deflake the ab-drive run-dir sweep counts against foreign temp dirs (#11360)

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

* fix(goal): revoke proposal approval when entering Plan mode

* fix(goal): give ACP proposal settlement one owner

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-10 15:43:28 +00:00
qqqys
c3d9bed85b
fix(serve): restore configured channels after restart (#11472)
* fix(serve): restore configured channels after restart

* fix(serve): make configured channel restore fail soft

* fix(serve): identify configured channel source

* fix(serve): harden configured channel recovery

* fix(serve): bound configured channel startup restoration

* fix(serve): preserve fail-soft channel startup

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
2026-09-10 15:40:24 +00:00
jinye
cbd2cbadec
fix(acp): Preserve submitted prompt provenance for auto recall (#11455)
* fix(acp): Preserve submitted prompt provenance for auto recall

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

* fix(acp): Omit submitted_prompt on channel-classified turns (#11455)

Channel turns are machine-relayed deliveries (loop jobs, webhook tasks,
adapter-synthesized events), so nothing on them crossed a submission
boundary. Since submitted_prompt presence alone gates Auto Recall's
outbound provider search, emitting it there ran retrieval on unattended
turns and could publish composed wrapper text as user provenance. Gate
emission on a positive user-submission condition instead.

Also reconcile the producer-set contract this PR changed: hooks.md, the
Auto Recall READMEs, the in-product hook description with its locale
mirrors, and the recording payload comment now state the actual rule,
and the new design doc records the channel exclusion plus the
strict-decoder rollout warning.

* fix(acp): Require explicit submission provenance for auto recall

Carry original Web Shell composer text separately through admission and queues; keep background dispatches and undeclared requests ineligible. Reconcile hook and integration contracts and add regression coverage.

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-10 15:17:51 +00:00
qqqys
57e828a215
refactor(dingtalk): remove obsolete background response aggregation (#11570)
Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
2026-09-10 13:59:31 +00:00
qqqys
b3ebe1e84b
feat(channels)!: remove message prefix filtering (#11571)
BREAKING CHANGE: messagePrefix no longer filters or rewrites channel messages. Existing sender, group, mention and pairing policies still apply.

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
2026-09-10 13:59:17 +00:00
jinye
dfcf07accc
feat(serve): support 256 workspaces by default (#11515)
* feat(serve): support 256 workspaces by default

Make workspace registration capacity configurable from 1 to 256 with a default of 256. Preserve independent channel and ACP child policies, bound persistent registrations, and keep the expanded global session default at 800.

Refs #11386

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

* fix(serve): correct capacity docs and diagnostics from review

- docs/users/qwen-serve.md documented --channels all; the flag is --channel.
- The promotion path lost its server-side diagnostic when an oversize store
  refused the write, while the sibling create path kept logging.
- The startup overflow message labelled explicit --workspace values as
  Restored, sending operators to a store that holds fewer records.
- Registration-capacity validation named the environment variable even when
  an embedder supplied an out-of-range option.
- The channel-owner message asserted retained owners were counted and told
  operators to stop a worker, neither of which holds at boot.
- Session-total and new-409 documentation still described pre-change
  behaviour, and the design table's line anchors drift on merge.
- Tests read the registration cap from the ambient environment, and neither
  the environment-driven cap, the legacy session derivation, the injected
  route cap, nor the internal-runtime exemption had a witness.

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-10 11:57:10 +00:00
Shaojin Wen
4c6e2adcea
feat(web-shell): render URLs in user messages as clickable links (#11464)
* feat(web-shell): render URLs in user messages as clickable links

User-typed URLs in the Web Shell transcript rendered as plain text, so
they could not be opened by click. Split message text with a small
tokenizer (explicit http(s) scheme only, CJK characters terminate a
match, trailing sentence punctuation and unmatched closing brackets are
trimmed, a bare scheme stays text) and render URL segments through a
LinkifiedText component that mirrors MarkdownLink's safety check and
desktop-shell external-open routing.

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

* fix(web-shell): harden user-message URL boundaries from review

Address review on #11464:

- Invert the tokenizer's character class from a hand-maintained denylist
  to an ASCII URL-grammar allowlist. The denylist absorbed unlisted
  scripts into the href (emoji and Thai shifted the host via IDNA) and
  truncated URLs containing an apostrophe (en.wikipedia.org/wiki/L'Aquila
  linked to the unrelated /wiki/L page). The allowlist fails closed and
  matches remark-gfm's boundary behavior.
- Trim markdown emphasis delimiters (*, _, backslash) from the match end
  so a pasted **url** does not request /docs**.
- Rewrite trimTrailing to count each bracket pair's excess closers once
  and trim by index, making the trim O(n) instead of O(n^2) on inputs
  with long runs of unmatched closers.
- Pin the host-parser parts path in UserMessage with a URL test; tag the
  Playwright spec @smoke so PR CI actually runs it; reuse Markdown's
  .link rule instead of copying it; fix the garbled backtick bullet in
  both design docs and record the raw-IRI prefix tradeoff.

Mutation witnesses: reverting the allowlist reddens 9 tokenizer tests,
dropping * _ backslash from TRAILING_PUNCT reddens the emphasis test,
and reverting the parsed-parts branch reddens the new UserMessage case.

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

* fix(web-shell): drop dead TRAILING_PUNCT entries and correct the gfm-parity claim

Address round-2 review on #11464:

- TRAILING_PUNCT held three entries the allowlist can never produce
  (double quote, backtick, backslash), so the round-1 commit message's
  backslash mutation witness was false. The set is now exactly the
  reachable trimmable chars, and the quote/backtick it.each cases are
  retargeted as termination tests (they pin the allowlist boundary, not
  the trimmer).
- The design docs' decision row claimed the allowlist matches remark-gfm
  boundary behavior; measurement says otherwise in both directions
  (trailing ~ kept, no left-boundary rule, stricter on trailing
  backtick/backslash, balanced a[0] keeps its ]). The row now says
  inspired-by and lists the known deltas, in both language versions.

Mutation witnesses: dropping * and _ from TRAILING_PUNCT reddens the
emphasis test; adding double quote to the allowlist reddens the new
termination test; the removed dead entries were verified to change no
test outcome (that is why they are dead).

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

* fix(web-shell): trim trailing tilde, normalize bare % in href, pin tokenizer coverage

Address round-3 review on #11464:

- Add GFM's strikethrough delimiter ~ to TRAILING_PUNCT so a pasted
  ~~https://github.com/QwenLM/qwen-code~~ does not 404 on click, while
  interior tildes (/~user) stay intact. The docs' delta list drops the
  now-fixed 'trailing ~ kept' entry and gains the measured character-
  reference divergence (&amp; kept verbatim where remark-gfm resolves it).
- Normalize a bare % (not followed by two hex digits) to %25 in the
  rendered href only — byte-identical to the assistant markdown path's
  normalizeUri — keeping the visible text verbatim; the docs' 'the one
  case' superlative is replaced by the measured wrong-link shapes.
- Pin the tokenizer axes the suite previously left green under mutation:
  interior membership for all 11 unpinned allowlist characters plus a
  host:port port fixture (dropping : now reddens two tests), termination
  for all seven excluded printable ASCII delimiters, the angle-bracket
  autolink form, and the no-left-boundary decision.

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

* fix(web-shell): match normalizeUri's % rule and pin the desktop opener's href

Address round-4 review on #11464:

- The bare-% normalization used a hex-only lookahead, while the assistant
  path's normalizeUri (mdast-util-to-hast) skips encoding when two ASCII
  alphanumerics follow — so 50%off diverged between user and assistant
  bubbles. The lookahead is widened to [0-9A-Za-z]{2}, measured identical
  to the real normalizeUri on every reachable input (space cannot appear
  in a match). The comment now also names the remaining delta: raw
  [ ] { } stay verbatim where normalizeUri percent-encodes them, and both
  design docs record it; the English IRI row's inverted claim about the
  assistant path is corrected (it does normalize).
- Pin that the desktop external opener receives the normalized href, not
  the raw matched text — the segment.value mutation survives all previous
  tests and now reddens the new desktop-click case.
- Correct the interior-fixture comment in linkify.test.ts: only six of the
  eleven characters are trimmed by TRAILING_PUNCT at a trailing position;
  the other five are interior for fixture uniformity.

Mutation witnesses: hex-only lookahead reddens the 50%off case; passing
segment.value to the opener reddens the desktop-click % case; both stay
green on the committed source.

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-10 11:26:08 +00:00
Shaojin Wen
76635386e4
fix(review): dispatch review agents through parallel workflows (#11524)
* fix(review): dispatch review agents through parallel workflows

* fix(review): respect workflow preferences and validate loaded scripts

* test(web-shell): align fixtures with branding and compact footer
2026-09-10 11:15:56 +00:00
易良
585589ac67
fix(dev): install hooks during worktree bootstrap (#11496)
* fix(dev): install hooks during worktree bootstrap

* fix(dev): keep worktree hook setup out of the shared git config

Husky runs `git config core.hooksPath .husky/_` with no --worktree, so
from a linked worktree the value lands in the config every worktree of
the repository shares while `.husky/_` is created only in the checkout
being bootstrapped. Skip the Husky step and report it when the key is
unset and this checkout does not own the repository config, so a
bootstrap can no longer repoint hook resolution for roots that never
received the wrappers. A primary checkout still installs hooks, and an
already-configured `core.hooksPath` is untouched.

Also drop the caller's success exit, which `install()` made unreachable
when it started exiting on every successful path, and bring the
pnpm-worktree-bootstrap design doc in line with a hook step it still
recorded as deliberately skipped.

The new fixture runs the real script against a throwaway root whose
`.git` is a file or a directory and whose config comes from a real
`git init` repo, which makes both new branches reachable and pins the
fail-closed guard: the injected `GIT_CONFIG_*` constant holds one value
for the child's whole lifetime and cannot express the unset state that
asks husky to write.

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

* fix(dev): ask git which root owns the worktree hook config

`ownsRepositoryConfig()` inferred repository-config ownership from the
filesystem shape of `.git`, and the proxy is wrong at both ends. With no
`.git` at all, `statSync(..., { throwIfNoEntry: false })` returns `undefined`
and the predicate folded that absence into "owns the config", so a
repository-less checkout ran husky into its `.git can't be found` soft failure
(exit 0) and the fail-closed check then turned a successful dependency install
into exit 1 blaming Husky. A `.git` file is not only a linked worktree either:
`git clone --separate-git-dir` checkouts and submodules have one too and do own
their config, so hooks were declined where they would have been correctly
scoped.

Ask git instead: `rev-parse --git-dir` differs from `--git-common-dir` only in
a linked worktree, and a failed `rev-parse` names the no-repository state, so
the skip notice stops asserting "linked worktree" and the bootstrap does not
gain a hard git dependency.

Also bind the fail-closed check to an artefact husky's own write produced, not
only to the config value. husky 9.1.7 exits 0 on every soft-failure path
(`index.js:16` git command not found, `index.js:17` refused `git config` write)
before the `mkdirSync(_())` on line 19, and a linked worktree inherits
`core.hooksPath` from the config it shares, so re-reading that value compared
it against itself and passed exactly when husky had created nothing.

Restore the registry-fallback case's hermeticity: `PATH` holds only the stub
directory again, which now also pins that the retry needs no ambient git.
Rebuild the ownership fixture from real git layouts, because `rev-parse`
resolves nothing for a `mkdirSync`'d `.git` or a hand-written `gitdir:` file,
and give the stub husky a failing mode so the exit code husky returns is pinned
rather than the install result's.

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

* fix(dev): surface hooks-path read failures in worktree bootstrap

- getHooksPath() no longer collapses a refused git config read (exit 128/2/3) into "unset": only an absent key (exit 1) or a missing git binary keeps the skip path; anything else fails the bootstrap with the read error instead of a green, hook-less worktree.

- The linked-worktree skip notice now names the recovery path: re-run this script once the primary checkout has hooks installed.

- Tests cover a git stub exiting 128, the real-world unset HUSKY state, and pin the recovery sentence in the skip notice.

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-10 09:44:47 +00:00
tanzhenxin
d030e51b49 chore: merge origin/main to include CI fixes 2026-09-10 17:24:22 +08:00
qqqys
0ef35351e9
feat(acp): register daemon-managed sessions in the session registry and let them send peer messages (#11488)
* feat(acp): register daemon-managed sessions in the session registry and let them send peer messages

Only the interactive UI registered, so a session `qwen serve` drives was
invisible to `qwen sessions ps`, absent from every peer's `list_agents`,
and unable to send at all: the send path reads this process's own record
to learn its reply address, and with no record it reported the feature
disabled. A user with a terminal open and a daemon session working in
the same repository could not have the two speak to each other.

The registry was keyed by PID, one record per process, which was right
while a process meant a session. The `qwen --acp` child a daemon spawns
hosts up to thirty-two at once, each with its own id, name and working
directory, and one record cannot describe them. So a process hosting
several writes one record per session, `<pid>-<8 hex>.json`. The suffix
is minted at registration and never moves — deliberately not derived
from the session id, because an id is swapped underneath a live session
by a `/clear` or a session load, and a record that renamed itself would
strand every reader holding the old name. The PID in front of the suffix
is still what liveness, the sweep and the namespace guards read, so a
minted record is reaped exactly like a shared one, and a reader too old
to know the name does not see it rather than mistaking it for litter.

Which record a caller means is a slot, threaded through patch,
unregister and the own-record read. A Config learns its slot from the
registration itself, so a session's `/clear`, its `/cd` and its exit
each touch its own record and no sibling's.

The process binds one inbox for all its sessions and writes that address
into every record. Senders already pin `toSessionId` on every frame, so
that is what tells the sessions apart; a frame naming a session this
process does not hold — or naming none, which used to be deliverable
because there was only one session it could have meant — is answered
`misaddressed`.

That sharing broke an assumption elsewhere. The send path and
`list_agents` excluded "this session" by matching either its id or its
reply address, the address being a proxy for the id that held while one
process bound one socket. Siblings share an inbox, so the proxy stopped
holding and every sibling of the sending session would have been
filtered out as if it were the sender. Identity is the session id alone
now; a twin under a second process is still excluded, because that is
what the id catches.

Inbound is refused rather than held. A hold is a question put to a
person, and nobody is watching a held-message list on a daemon-managed
session's behalf, so holding would leave every sender waiting out a
five-minute expiry to learn nothing. Where a held message should surface
for these sessions is a real question and separate work; the protocol
page says so rather than leaving a reader to find out.

Registration happens only while cross-session messaging is on. The
interactive UI registers unconditionally because its record also answers
"what is running right now"; a daemon-managed session's record exists to
be addressed, and one with no inbox behind it would put a name in every
peer's listing that can be addressed and never answered.

The inbox is bound by the first session that needs one, and the
transport is imported at that moment, so an ACP process that never
messages a peer never loads it. Closing is registered as exit cleanup
and also runs from both teardown methods, because a bare signal reaches
neither. Whether to bind at all is read from the settings the process
started with: the agent's `settings` field is re-pointed at whichever
session is being handled, and this is a question about the process.

Part of #10925
Unblocks #10118 (M3)

* fix(acp): import the peer-messaging transport statically so the fast-path bundle closure stays clean

The agent imported the transport and `registerSession` with dynamic
`import()`s, on the theory that a process with messaging off should not
load them. The theory bought nothing and cost something: every module
behind both imports already sits in the agent's static closure, and a
dynamic import of the core barrel turns that barrel into a code-splitting
entry. esbuild then re-partitions the shared chunks, and one of the
chunks the agent imports statically came to hold iconv-lite's encoding
tables — a module the ACP fast path must not load, which is exactly what
the startup bundle closure check exists to catch, and did.

Both imports are static again, the catch-up loop that existed only to
cover the import's latency is gone, and the design note says why the
transport is loaded the way it is.

Part of #10925

* fix(acp): judge hosted sessions by live ids and their own settings

Review follow-ups on daemon-managed session registration:

- ownsSessionId tested the frozen publication key, so a frame pinned to
  the id the record actually advertises after /clear was answered
  misaddressed; test each hosted Config's live id as well.
- Registration and the inbox bind followed the process's startup
  settings; each session's own settings decide instead.
- A failed inbox bind latched as started, so no session ever retried
  it; treat null/failure as not-started, and latch closed on teardown
  so a retry cannot resurrect the socket.
- disposeSessions awaited the inbox drain before aborting generations;
  abort first so a running turn cannot keep executing tools after the
  client is gone.
- The send-side mirror re-minted its bucket per addressed session id
  while the receiver meters per address, letting alternating siblings
  reset the allowance and duplicate window; keep one bucket per inbox
  address and stop the repeat refusal naming "that session".
- The self-exclusion could be beaten by a re-id patch landing between
  the own-record read and the directory read; re-check a peer on this
  session's own inbox against a fresh read, on the send path and in
  list_agents alike.
- The record filename check accepted a zero-padded pid on platforms
  without a namespace id; require the canonical decimal form.
- Drop the dead sessionId parameter from the misaddressed-frame
  re-assert, document the two record filename forms and the one-sender
  consequence, and add the Chinese design doc.

* fix(ipc): probe each inbox once, and keep a minted record nameable

Two behaviour fixes from review, plus the tests that would have caught
them and the cases the slot plumbing was missing.

A process hosting several sessions writes one record per session, all
pointing at the one inbox. The liveness pass dialled that socket once
per record, so a daemon with 32 hosted sessions opened 32 simultaneous
connections at one address every time a peer listed the directory —
enough to trip the inbox backlog. Probe each distinct address once and
apply the verdict to every record that names it.

Re-registration deleted the captured path of a minted record before it
knew whether the delete would happen. On a transient read error it
returned early with the capture already gone, and a minted suffix cannot
be derived a second time: the record stayed on disk under a name this
process could no longer patch or remove, outliving the session as a
directory entry pointing at a dead inbox. Keep the capture on that one
branch.

Tests: every registration fixture returned the shared slot, which is
also the parameter default, so nothing could tell "passes the slot it
registered under" from "passes the default" — config and send_message
now register a minted slot and name it in the assertion. Also covers
the unknown-slot path against a live shared record, a minted filename
surviving an id swap, near-miss record filenames, probe-once, the
ownsSessionId branches of the inbound gate, and an ACP host running two
sessions through one inbox.

* test(core): name the minted slot in the list_agents identity read

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-09-10 08:30:36 +00:00
tanzhenxin
eabad6ed95 chore: merge origin/main into codex/model-api-selection 2026-09-10 15:33:01 +08:00
易良
11e629b3a1
perf(export): split the transcript renderer's embedded CSS into a versioned asset (#11485)
* perf(export): split the transcript renderer's embedded CSS into a versioned asset

The export renderer carried the web-shell component stylesheet as a ~2.3 MB
string literal, so every reader parsed and compiled 4.1 MB of JS (56% of it dead
CSS) before a transcript could render. Lift that literal out at export build
time into a version-pinned, SRI-protected export-transcript-document.css served
from unpkg and loaded via a nonce-bearing <link>, dropping the renderer JS to
~1.83 MB.

The transform is an esbuild onLoad plugin in the web-templates export build that
strips the injected CSS constant from web-shell's dist/transcript.js; web-shell
source and runtime behavior are untouched. The document's fail-closed load-error
path is extended to the stylesheet so a missing CSS asset fails the same way as
a missing renderer.

* fix(export): match the transcript CSS entry on Windows paths too

esbuild hands plugin callbacks the platform-native absolute path, so the
extract-transcript-css `onLoad` filter never matched on Windows: the callback
did not run, `extractedTranscriptCss.css` stayed undefined, and the mandatory
extraction guard below aborted the build. That build is not platform-gated —
`scripts/prepare.js` runs it from `prepare`, so `npm ci` itself would fail on
every Windows contributor and on the windows-latest legs of test_windows and
desktop-release.

Widen the separator to `[\\/]`, keeping the `transcript\.js$` tail so the
barred `web-shell/dist/index.js` package root still does not match. The filter
moves to transcript-css-entry.mjs because build.mjs is a top-level-await script
with no harness — the same reason scripts/sdk-node-exporter-stub.js exists — so
scripts/tests/transcript-css-entry-filter.test.js can pin both separators.

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

* fix(export): latch a stylesheet failure ahead of the parser block

The `<link id="transcript-stylesheet">` sits in `<head>` while the `window`
error listener that catches its failure is registered by an inline script in
`<body>`. Chromium parser-blocks that script on the pending stylesheet, so when
the CSS failure settles first the error event is dispatched with no listener to
receive it: nothing marks the render as failed, both renderComplete guards in
document-main.tsx pass, React mounts the transcript without any of the
component CSS, and the requestAnimationFrame stamps
`data-render-complete="true"`. The reviewer measured this fail-open above
roughly 2.1 MB of document HTML (272 of the 1,000 permitted blocks) for a 404,
an SRI rejection, a truncated body and a destroyed socket alike, and fail-closed
for a *late* failure — so size, not failure kind, decides it.

Latch the failure in `<head>` before the `<link>` is parsed and act on the latch
from the existing body IIFE. The head script only records: `showLoadError()`
writes `document.body.dataset` and `#app`, neither of which exists while the
parser is still in `<head>`. It carries `nonce="__EXPORT_NONCE__"` because the
document CSP allows no inline script, which is safe — `formatters/html.ts:53`
replaces every occurrence. The listener is capture-phase because resource error
events do not bubble.

Not the `link.sheet === null` variant: the reviewer measured `sheet` non-null
for a 404, a truncated body and a destroyed socket, so it only detects SRI
rejection.

scripts/tests/export-transcript-document-template.test.js pins the position,
the nonce, the capture phase and the record-only shape; all five cases go red
against the unpatched template. The behavioural witness (real Chromium, large
document, instant CSS abort) belongs to the playwright transcript gate, which
is out of budget on this host.

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

* fix(scripts): name the missing export renderer assets, pin the CSS gate

The bundle copy became all-or-nothing over two artifacts but its `else` warning
still named only the renderer, so the one new way to reach that branch — a tree
built before the split, then `npm run bundle`d without rebuilding web-templates,
which has the JS and no CSS — told the operator to go looking for a
`export-transcript-document.js` that was sitting right there, and silently
discarded it. List the paths that are actually absent, matching the sibling Web
Shell warning twenty lines above. Stays warn-and-skip: prepare-package.js is the
release gate.

Also pin that release gate. Every fixture that reached `preparePackage` staged
`dist/export-transcript-document.css` unconditionally, so deleting the new
required-path entry left the whole test:scripts lane green; a release built with
`npm ci --ignore-scripts` would then publish documents whose stylesheet 404s on
unpkg for that version. `verifyBundleArtifacts` reports through console.error +
process.exit(1) rather than a throw, so the new case stubs exit instead of
copying the audio-capture sibling's `toThrow` idiom.

Both cases were flip-checked: restoring the old warning text, and deleting the
CSS line from prepare-package.js, each turn their case red.

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

* fix(export): pin the stylesheet-failure id contract and sync the design docs

The <head> latch, the body listener and the <link> each spell
'transcript-stylesheet' independently and nothing compared them, so renaming
either listener's id left the whole suite green while the latch recorded
nothing - reinstating the fail-open the latch was added to close. Derive the id
from the <link> and assert both listeners compare against it. Verified red under
both mutations: latch id -> 'transcript-renderer' (1 failed | 5 passed), and the
mirror with the body listener's id wrong and the latch intact (same).

Both design docs still specified the two shapes the previous round replaced: the
forward-slash-only onLoad filter that never matches on Windows, and the
body-listener-only fail-closed extension. Section 1 now quotes the shipped
TRANSCRIPT_CSS_ENTRY_FILTER and names transcript-css-entry.mjs, section 2
describes the <head> latch (position, nonce, capture phase, record-only),
section 3 names the module-level render guard, and "Files affected" lists the
three omitted files. EN and zh-CN are updated in the same commit.

Also correct the shape-guard comment in build.mjs: the document nonces every
<style> created through document.createElement, so the CSP would not block an
un-stripped duplicate, and a 367-byte regrowth stays inside both byte budgets.
That throw is the only guard on the duplicate-injection path.

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

* fix(export): close out transcript CSS review comments

* fix(export): tighten transcript CSS closeout

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-10 07:15:32 +00:00
tanzhenxin
1af6094b77 feat: select the OpenAI API per model 2026-09-10 15:11:57 +08:00
易良
b1ac3e2970
docs(daemon): add a REST integration entry point, guarded against the route surface (#11477)
* docs(daemon): add a REST integration entry point

The daemon REST surface is documented — 3.5k lines of route reference plus 21
deep-dive pages, all published. What is missing is the way in. An integrator
lands on a 237-route surface with no signal about which routes are meant for
them, and `users/overview.md` answers "how do I start using the CLI", not "I am
putting this inside my product".

Add one page that answers the second question:

- Which of the six daemon integration paths to take, and what each one leaves
  the integrator owning.
- The 25 routes a REST integration actually uses, grouped by job and linked to
  their reference sections — against 237 registered, the rest being Web Shell
  internals that change with that UI.
- The minimal flow end to end: preflight, create, subscribe, prompt, answer
  permissions, close.
- Two facts that are load-bearing at design time and currently buried: the
  daemon spawns `qwen --acp` children so the CLI must be on its host, and the
  auth model is single-operator with no per-end-user principal.

This deliberately links rather than restates. The reference stays the authority;
17 of the 25 routes have a dedicated section there and the page says plainly
which 8 do not, rather than papering over it (#11359).

Refs #11427, #11359

* test(serve): guard the REST guide's promises against the route surface

The guide tells integrators a specific 25-route subset is what they should
build on, and links 17 of them into the protocol reference. Both claims are
hand-maintained against a 237-route surface, so a rename upstream would leave
the page promising a route that no longer answers, or pointing at an anchor
that no longer exists, with nothing failing.

Four checks, same shape as capabilities-docs-contract.test.ts:

- every route the guide names is still registered somewhere under serve/
- every protocol-reference anchor the guide links resolves to a real heading
- the 8 routes the guide marks "no dedicated section yet" are exactly the 8
  that lack one, so writing a section forces the note to be removed
- the source walker still sees >100 routes, so the first check cannot pass
  vacuously if registrations move to a style the regex misses

Scope is the guide's own promises, not an inventory of all 237. A full
coverage ratchet would need a ~174-entry "known undocumented" baseline; that is
bulk in service of a problem nobody has reported, and can be added if the
undocumented set starts moving.

Verified statically against this tree: all four pass, and the walker sees 213
routes.

Refs #11427, #11359

* docs(daemon): correct the process model and the memory flags

Review on #11477 caught two accuracy defects and two smaller ones. All four
verified against source before fixing.

**The process model was stated backwards.** The page said each session's agent
gets its own process and a crash is contained to one session. It is one child
per *bridge*, and one bridge per *workspace runtime*: "At most one `qwen --acp`
child per bridge… Multiple sessions multiplex onto the child via
`connection.newSession()`" (bridge.ts), and the single `channel.exited` cleanup
"tears down ALL multiplexed sessions" (ensureChannel). `daemon/00-index.md` and
`users/qwen-serve.md` already said so. The container-sizing advice was wrong in
the same direction, and "crash contained to one session" inverted the fault
domain — it is the workspace. Rewritten, with the real lever named: run
separate daemons, since `--max-sessions` caps concurrency, not blast radius.

**The memory row read as enforcement.** Both flags are observe-only:
`--memory-budget-mb` "does not change how any `qwen --acp` child is sized", and
`--child-heap-mode` reports a modelled partition where "Nothing is applied: no
child is sized from the budget and no spawn is refused".

**Dropped the `stopReason` enum** rather than repeating it. Source has
`end_turn`, `cancelled`, `error`, `max_tokens`; `refusal` and
`max_turn_requests` also exist in packages/ but not in the protocol doc, and
the `length` the protocol doc lists appears nowhere in source. Copying a list
that is already wrong upstream would make a fourth copy of it — the page now
links to the route section instead. The upstream enum still needs fixing
(#11359).

**Dropped the hardcoded "237 routes".** It disagreed with this PR's own test —
237 counts method+path pairs, the walker counts 213 distinct paths — and
volatile totals in prose are what capabilities-docs-contract.test.ts exists to
discourage. The point stands without a number.

Contract assertions re-verified after the edits: 0 unregistered routes, 0 broken
anchors, the undocumented set is still exactly 8, walker sees 213.

* docs(daemon): correct the stopReason values in the protocol reference

The list under `POST /session/:id/prompt` was wrong in both directions. It gave
`end_turn`, `cancelled`, `max_tokens`, `error`, `length`.

Against source:

- `length` appears nowhere in `packages/`. It is not an ACP `StopReason` and
  the daemon never emits it.
- `max_turn_requests` and `refusal` were missing. Both are in the ACP union
  (`@agentclientprotocol/sdk` `schema/types.gen.d.ts`: `StopReason = "end_turn"
  | "max_tokens" | "max_turn_requests" | "refusal" | "cancelled"`), so they
  arrive from the agent even though qwen-code never constructs them itself.
- `error` is real but is ours, not ACP's — `create-sub-session.ts:695` and
  `live-session-coordinator.ts:1659` set it when a turn fails inside the daemon.
- `reconstructed_from_transcript` was missing entirely
  (`prompt-terminal-ledger.ts:295`), for turns recovered from persisted history.

Rewritten to separate the ACP values from the two the daemon originates, and to
say plainly that the field is an open string — it is typed `string` on the wire
(`bridgeTypes.ts:1258`), the ACP set can grow, and a client that exhaustively
switches on it breaks on the next addition. That last part is the guidance the
old list quietly worked against.

Found while fixing #11477's own copy of this list, which is now a link rather
than a fourth transcription of it.

Refs #11359

* docs(daemon): correct six daemon claims in the REST guide and protocol reference

Each claim below was checked against the code at this head and was stated the
wrong way round.

- qwen-serve-protocol.md: `turn_complete.data.stopReason` never carries `error`
  or `reconstructed_from_transcript`. A daemon-side failure publishes
  `turn_error` with its `code`/`errorKind`; the two `stopReason = 'error'` sites
  are SSE consumers' local variables (create-sub-session.ts:695,
  live-session-coordinator.ts:1659), and `reconstructed_from_transcript` is a
  ledger record (prompt-terminal-ledger.ts:295) that surfaces only as
  `promptTerminals[].stopReason` on POST /session/:id/load. The ACP five and the
  open-string instruction are kept.
- SDK row: the Python SDK has no daemon transport at all (sdk-python.md:15-16),
  so the row names TS and Java and states the gap instead of implying it away.
- Step 3: `model_switch_failed` is the only signal on the attach path; a fresh
  create also returns `modelApplied` in the 200 body (bridge.ts:5755-5816 ->
  session.ts:3563). The attach return object has no such field
  (bridge.ts:9828-9845) and swallows the switch deliberately (:9794-9806).
- Step 5: the permission timeout defaults to 0 = wait indefinitely
  (bridge.ts:2726, commands/serve.ts:623-628); it does not "fire".
- Step 5 + Permissions table: lead with the session-scoped vote route.
  POST /permission/:requestId is wired to primaryBridge only
  (routes/permission.ts:107-121, server.ts:3286-3287), so it 404s for a session
  owned by another registered runtime, with the same body as a lost vote.
- Operations memory row: `--memory-budget-mb` has a live consumer — the
  adaptive live-journal growth pool (daemon-memory-budget.ts:19,37,38 ->
  run-qwen-serve.ts:3802-3812 -> bridge.ts:2912) — so it is not observe-only.

The route/anchor contract in rest-integration-docs-contract.test.ts is
unchanged: no route renamed, no anchor link added or removed, and the
session-scoped permission route stays outside the protocol reference so the
pinned undocumented eight still holds.

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

* docs(daemon): correct the turn_error, promptTerminals, modelApplied and approval-mode claims

Four review findings, one defect class: a documented claim the code does not
back. Docs only — no product code touched.

- `turn_error`: `data` always carries `message`; `code` only when the daemon
  classified the failure, and `errorKind` on none of the three named paths. A
  prompt rejected because the ACP child died mid-request carries neither
  (bridge.ts:2119-2120 conditional spreads, :1949-1956 extractErrorCode needs a
  `code` property, :2065-2071 classifyTurnErrorKind matches only 'terminated',
  status.ts:100-107 BridgeChannelClosedError has none). Teardown flush and
  deadline do carry `code` (:2434, :10303).
- `promptTerminals`: reconciliation appends two record shapes and the
  interrupted one has no `stopReason` key at all, so the reader is keyed on
  `terminal` instead (prompt-terminal-ledger.ts:283-297). The field is omitted
  entirely when the ledger holds no evidence (:374) and is attached only for
  `load` (routes/session.ts:4471-4478).
- The deadline paragraph four lines below named
  `errorKind: "prompt_deadline_exceeded"`; the publisher sets that value as
  `code` (bridge.ts:10300-10306).
- `modelApplied`: assigned only inside `if (modelServiceId)` and spread only
  when defined (bridge.ts:5755-5766, :5816), so step 2's bare create body
  yields a `200` with no such key.
- `permission_request`: the daemon pins no approval mode at spawn
  (acp-child-extra-args.ts:15-20, spawnChannel.ts:484-492), the settings default
  is ApprovalMode.AUTO (settingsSchema.ts:2905, config.ts:1642), and AUTO's
  filter skips requestPermission for the allowed class
  (Session.ts:12346-12354) — so auto-approval is the default posture, not a
  decision. An untrusted folder is forced to DEFAULT (config.ts:1646-1655) and
  /capabilities reports the vote policy only (routes/capabilities.ts:101-104).

Verified: packages/cli rest-integration-docs-contract.test.ts 4 passed, and
prettier --check clean on both files.

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

* docs(daemon): correct the REST integration guide's route, token and sizing claims

* docs(daemon): correct REST integration review findings

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

* test(serve): check guide tables and documentation targets

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-10 06:49:42 +00:00
Shaojin Wen
13b69b3313
feat(web-shell): make the product name and logo configurable (#11244)
* feat(web-shell): make the product name and logo configurable

White-labelling the Web Shell meant editing six source files and rebuilding: the
product name was a literal in the sidebar, the welcome header, the About panel
and the document title, and the logo existed as two hand-duplicated copies of the
same inline SVG.

Brand is now configuration. A name and a logo path are read from the operator
settings scopes, and a new process-global daemon route resolves the logo into a
data URI for the client. Embedding hosts get the same control through a prop that
replaces the resolved value outright. The terminal banner already had its own
white-label settings, so this follows their sanitization and path-resolution
conventions rather than inventing parallel ones.

Two decisions carry the design. The workspace settings layer is never read, so a
repository's own settings file cannot rename the product or name a file for the
daemon to inline into every connected browser. And a custom logo is only ever
rendered as an image, never injected as markup, which is what makes it safe for
the daemon to skip sanitizing the SVG it read.

An untouched installation renders exactly what it did before: the resolved brand
is empty, every consumer falls back to its own built-in literal, and the served
document keeps its original title and favicon bytes, so the existing visual
baselines stay valid.

* fix(docs): sync the daemon capability tag count with the new brand tag

The daemon index states how many capability tags the registry holds, and a
contract test fails the build when the two disagree. Adding the brand tag made
the registry 158 while the index still said 157.

* chore: re-trigger CI on the current head

The previous push arrived while a re-run of the prior commit was still holding
the workflow's concurrency slot, so GitHub suppressed the new run instead of
queueing it and the event does not replay once the slot frees. This leaves the
branch head with no CI at all. An empty commit re-fires the synchronize event;
the repository squash-merges, so it does not survive into main.

* fix(web-shell): Address brand review feedback across resolver, client and docs

- Expose a brandSettled flag from the workspace provider so a
  settled-with-no-brand outcome (older daemon, withdrawn host prop) is
  reported and invalidates stale cached chrome, while the in-flight
  state still fires nothing
- Fall back to the built-in sidebar mark when a logo data URI fails to
  decode, and when a host logo node is falsy; remount the logo image
  per data URI so a decode failure cannot stick to the next logo
- Report an empty host brand name as unset in onBrandResolved, and type
  the callback payload as the document-actionable subset
- Exclude the favicon from the boot watchdog's fatal-resource
  classification and keep its content-free error out of the panel list
- Require a namespaced <svg> root in the resolver, cover the post-decode
  size cap, and pin the exact stderr prefix the protocol doc publishes
- Assert GET /brand answers JSON on the real app for any Accept header
- Sync the capability index count (159), settings docs and the design
  doc with the above

* fix(web-shell): Address second brand review round — settle semantics, SVG scanner, test fixtures

- Settle the brand fetch only on a definitive outcome: an answer, or a
  404 from a route-less daemon. A retryable failure (503 bootstrap, 429,
  transport, old SDK) is unknown rather than absent — settling it
  cleared the pre-paint cache and reset the tab title mid-session over
  a blip nothing retries (review Critical)
- Rewrite the SVG root scanner quote-aware: a quoted `>`, `[` or `]` in
  DOCTYPE literals or the root tag no longer rejects valid documents,
  and an xmlns-shaped substring inside another attribute's value no
  longer certifies a namespace-less document as SVG
- Warn (not reject) when the root svg carries no viewBox and no
  width/height: the browser loads such an image successfully but may
  paint a blank mark at the sidebar's fixed size, and only the daemon's
  stderr can say so
- Treat a nullish host brand prop as absent in the resolution gate,
  matching the `??` one line above
- Pin previously unwitnessed guards: the fd identity re-check and both
  fs soft-fail branches, the logo key remount, the favicon error-list
  exclusion, the unsettled-to-settled transition, whole-object host
  takeover against a daemon logo, and the superseded-client reset and
  catch-leg disposed guards
- Drive GET /brand on a real app with the SPA fallback mounted and a
  token configured, and pin the System settings layer to empty files so
  a maintainer's machine-wide brand cannot leak into the assertions
- Round-trip the pre-paint cache through the real inline script instead
  of pinning each side against its own copy of the key
- Document the hard-link and decoded-size rules, the namespace
  requirement, the viewBox advisory, and the corrected 503 semantics
  (bootstrap catch-all, retryable; a draining daemon does not reject
  this route); the README's onBrandResolved row now names the narrower
  payload type

* fix(web-shell): Close third brand review round — placeholder exclusion, scanner correctness, retry path

- Read brand keys from the pre-substitution snapshot and refuse
  environment placeholders with a warning: placeholder substitution
  draws from the process-wide environment, which a workspace populates
  first at boot, so resolving brand placeholders would let a repository
  supply the value — the workspace exclusion through a side door
  (review Critical)
- Blank quoted SVG attribute spans per UTF-16 code unit, not per code
  point: an astral character collapsed to one space and shifted every
  later index, refusing valid namespaced documents (review Critical)
- Accept prefix-bound <svg:svg> roots that bind xmlns:svg to the SVG
  namespace (browsers render them), keep refusing <svgfoo> near-misses
  and prefix-only bindings on an unprefixed root, and pin the
  whitespace-prefixed fake-xmlns shape that actually exercises the
  quote-blanking
- Make the scaling-geometry advisory value-aware: an empty viewBox,
  zero dimensions, or percentage width/height now warns (loads
  successfully but cannot scale into the sidebar box)
- Add refreshBrand beside refreshCapabilities — same 404-only settle
  rule, generation-guarded, gated to the genuinely-missing state and
  no in-flight fetch — and call it from the connection recovery path,
  so a retryable brand failure no longer leaves in-app chrome and tab
  chrome disagreeing for the page's lifetime
- Warn on console when the sidebar logo fails to decode (the one logo
  failure the daemon's root-tag validation cannot see)
- Pin the O_NONBLOCK FIFO defence with a real mkfifo fixture, the
  published 429/drain transport states on the real app, and make the
  ordering test's body discriminating with a fixture brand
- Document the placeholder carve-out, prefix-bound roots and the
  value-aware advisory across settings docs, schema, protocol reference
  and the design record

* fix(web-shell): Close fourth brand review round — env-var side door, retry machinery, scanner class fix

- Add QWEN_CODE_SYSTEM_SETTINGS_PATH / QWEN_CODE_SYSTEM_DEFAULTS_PATH to
  the hardcoded project-.env exclusions: they select which file becomes
  the System settings layer, so a repository could otherwise promote
  its own file above the operator's User layer and rebrand the shell
  with literal values (review Critical)
- Refuse brand placeholders on substitution, not syntax: run the real
  resolveEnvVarsInString and refuse only when it would change the
  value, so a literal name containing `$` survives while anything a
  workspace-tainted environment could have supplied is refused
- Emit one stderr line per brand warning instead of a joined line, so
  a log rule keyed on one key's prefix keeps firing when another
  misconfiguration exists
- Make the SVG root matcher prefix-agnostic and entity-aware: any
  prefix bound to the SVG namespace qualifies, character references in
  attribute values are decoded before comparison, a degenerate viewBox
  no longer suppresses the scaling advisory, and a prefix-bound root
  with unprefixed children gets its own advisory
- Retry a retryable brand failure once after a short delay and warn
  only when the retry is also refused (404 stays silent); wire
  refreshBrand into the recovery path with tests; gate it on
  in-flight; attribute retries per client instance; reset the brand in
  the render that observes a client change so no committed frame mixes
  two connections
- Pin the O_NONBLOCK flag with a flags-capture witness (the FIFO case
  alone cannot fail fast: a blocked open wedges the worker's event
  loop), skip the FIFO fixture on Windows where the defence does not
  exist, and mock the not-stattable path instead of relying on POSIX
  ENOTDIR
- Document the startup hold vs terminal 503 semantics correctly and
  retire the design record's pre-retry narrative

* fix(web-shell): Close fifth brand review round — real XML parser, docs alignment, hold test

- Parse the logo with the vendored fast-xml-parser instead of the
  hand-rolled root-tag scanner: well-formedness (duplicate attributes,
  misplaced declarations) is now rejected by a real validator, the
  namespace check reads parsed attributes with entity references
  decoded, any prefix bound to the SVG namespace qualifies, and the
  prefix-children advisory walks the parsed tree — construct-aware by
  construction. The DOCTYPE is stripped for the validator with a
  quote- and subset-aware scan because the validator's own DOCTYPE
  handling is quote-blind (review Suggestion, filed as the class-level
  finding)
- Bound entity decoding at Unicode's ceiling: an out-of-range
  character reference no longer throws a RangeError out of the
  resolver — which the route's catch-all turned into a 200 {} that
  dropped the validly configured brand NAME along with the logo
  (review Critical)
- Refuse brand placeholders by comparing the layer's substituted value
  against its pre-substitution snapshot — the authoritative engine
  WITH the home-.env fallback — and keep an unresolvable placeholder
  verbatim so a typo'd variable shows as literal text; say so in all
  five published artifacts, and hint when a missing logo's path still
  contains a placeholder token (review Critical)
- Gate the prefix-children advisory on the default namespace being
  absent: with both bindings, unprefixed children render fine
- Split the scaling advisory's message by cause (zero-area viewport,
  malformed viewBox, no usable geometry) and document both advisories
- Pin the startup-hold guarantee with a real deferred-runtime test:
  GET /brand issued before the runtime is ready is held and answered
  200 once it settles
- Drive the two retry tests on fake timers (BRAND_RETRY_DELAY_MS is
  exported for them) instead of sleeping through the production
  constant, and pin the route's one-stderr-line-per-cause invariant
  with a two-misconfiguration case

* fix(web-shell): Close sixth brand review round — saxes parser, lockfiles

Swap fast-xml-parser (subset validator plus pre-decoded attributes) for
saxes, the streaming XML parser jsdom uses: malformed documents are now
refused instead of shipped, attribute values are entity-decoded exactly
once so the xmlns gate cannot be faked by a second decode, and the parse
stream is closed after write so an unclosed root fails at EOF. Regenerate
both lockfiles for the dependency swap — the round-5 fast-xml-parser edge
was never recorded, which is what turned the frozen pnpm installs red —
and pin the refused well-formedness classes and the double-decode fixture
in the resolver tests.

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

* fix(web-shell): Enable namespace processing in the brand logo parser

saxes was constructed with default options, so an undeclared namespace
prefix on a child (the classic legacy-export xlink:href with no
xmlns:xlink) parsed clean here while browsers fatal on it — the daemon
shipped a data URI no browser renders, with nothing on stderr. Enable
xmlns: true, matching jsdom (browsers always have namespace support),
flatten the object-shaped attribute values that mode returns, and pin
both sides: undeclared child prefixes refuse, implicitly-bound xml: and
properly declared bindings still accept. Mutation-checked: reverting the
option and the flattening turns the new refusal test red.

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-10 04:54:45 +00:00
Shaojin Wen
89da433888
feat: delegate a subagent turn to an external agent over ACP (Claude Code first) (#11003)
* feat: delegate a subagent turn to an external agent over ACP

A subagent definition can now declare an `executor` block naming an external
agent process. The turn runs there over ACP and is re-published as the same
AgentEventEmitter events the in-process path emits, so the JSONL transcript
writer, SubAgentTracker's nested-permission bridge, virtual subagent sessions
and the Web Shell subagent panel all work unchanged. Claude Code is the first
agent wired up.

Delegating rather than exposing a foreign agent as a peer session backend keeps
the parent Qwen session authoritative: no re-keying of workspace identity from
cwd to (cwd, backend), and no change to the bridge's one-channel-per-runtime
invariants. Agent definition files already mirror Claude Code's
`.claude/agents/*.md` schema verbatim, so this extends that compatibility from
the definition layer to the execution layer.

Core side: a `SubagentExecutor` interface narrowed to the members production
callers actually use, which `AgentHeadless` now implements so drift fails at
compile time; `createAgentHeadless` returns the interface and dispatches on
`config.executor`; `AgentCore.buildChatSystemPrompt` is extracted as
`renderSubagentSystemPrompt` (a verbatim move, the private method delegates) so
an executor that never builds an AgentCore produces a byte-identical prompt;
`dispose?()` is composed into the returned dispose so the external process
cannot outlive its subagent; and the host factory is injected through
`Config.setExternalAgentExecutor`, following the existing
`setSessionWorkflowEnabledProvider` inversion, so core gains no ACP dependency.
The types a host needs are exposed on a narrow `./subagentRuntime` subpath
rather than the package root. Because the key (`subagentRuntime`) does not mirror
its source stem (`subagent-runtime.ts`), the wildcard cannot resolve it, so every
resolution map that fronts core's exports carries an explicit entry: the cli
`tsconfig` paths and vitest alias, `integration-tests/tsconfig.json` paths, and
the skill-review-harness loader's named map. A cross-package contract test pins
the export entry, the barrel's re-exports and the cli resolutions; the repo's own
`integration-tsconfig-core-paths-sync` and `text-capture-core-loader-sync` guards
pin the other two, so a future named export that forgets a mirror fails CI rather
than silently falling through to a stale `dist`.

Everything that could silently substitute a different agent, or grant authority
nobody approved, fails loud instead. Three bypass paths are closed. Frontmatter
rejects a malformed `executor` block outright rather than dropping it — dropping
it leaves `config.executor` undefined, so no downstream check engages and the
turn runs in-process under Qwen's model with nothing on stdout, stderr or the
debug log — and the block is re-validated at the consumption point, because
session-level subagents are injected as plain objects and bypass frontmatter
parsing entirely, and at serialization, so a save/reload round trip cannot drop
the backend. An executor-bearing file whose frontmatter YAML does not parse is
rejected too, but only such a file: the validation reads the original document
node and refuses when `parseDocument` reports an error AND the file claims a
top-level `executor` — by AST key, by the sanitized frontmatter value, or by a
raw-text probe anchored to column 0 with optional quotes, so a quoted
`"executor":` is caught (the lenient parser keeps the quotes in the key and a
repaired `parseDocument` nests it, so both miss it) while an indented `executor:`
under `mcpServers:` or inside a `description: |` block scalar is not (it declares
nothing and must not hard-refuse an unrelated definition). The error must also be
able to reach the executor. The guard refuses when `parseDocument` — the real
YAML parser — lost a text-claimed executor key (`!hasExecutor`): YAML errors are
not line-local, so a compact-mapping error on an earlier line drops the whole
remainder, and the lenient `parseSimple` fallback must not be trusted to rebuild
it — it is a line-based heuristic that mangles block scalars and sequences,
turning `command: |` plus an indented `npx` into `command: '|'` and so spawning
an executable literally named `|` that the file never declared. It also refuses
when `parseDocument` kept the node but an error sits at or after the executor's
line, where a repair could rewrite it into a different command/args. Only an
error strictly before the executor line that `parseDocument` survived
(`hasExecutor` still true) leaves the subtree byte-faithful, so a valid
external-agent definition — e.g. one carrying a duplicate `name:` from a bad
merge — keeps loading instead of vanishing from `/agents` over an unrelated
quirk. And because `parseDocument` tolerates an unresolved YAML alias
(`command: *undef`) with an *empty* `document.errors` while `toJS()` throws when
it resolves the node, the `toJS()` read is wrapped so that case is refused as an
invalid executor block rather than escaping parseSubagentContent as a raw parse
error the errors-based guard cannot see. For a real
executor the lenient parser would otherwise repair a malformed document into a
node that dispatches a different command/args than the file declares, or drops
the key and runs in-process. Every other definition keeps loading through the shared
parser's lenient `parseSimple` fallback — a description containing a colon, a
tab-indented field, or a duplicate key from a bad merge must not silently vanish
from `/agents`. The refusal message carries `invalid executor block` so
`warnInvalidSubagentFile` surfaces it on stderr instead of the debug log.
`convertToRuntimeConfig` rejects executor definitions,
which is the path team and background spawning use. Workflow `agent()` rejects
them before spawning, because token budgets, schema output and workflow tool
restrictions cannot be enforced across the process boundary — that covers bounded
and unbounded workflows alike. Cold resume is denied for a transcript carrying
`executor: 'acp'` or a legacy `external-acp:*` model label; the label is
deny-only and never selects an executor. With no factory registered, `create()`
throws before warming providers or resolving a model, and an ordinary Error from
an external factory propagates unwrapped instead of being relabelled as an
AgentHeadless construction failure. This deliberately diverges from the lenient
drop used for mcpServers and hooks: losing those degrades a capability, losing
this one substitutes a different agent.

The permission mode is the host's effective, already-clamped approval policy —
not the definition's raw request and never the external agent's own config. The
Agent tool resolves the definition's `approvalMode` against the parent session's
mode and folder trust and stamps the result onto the runtime context, and the
manager reads that resolved value back when it dispatches, so a definition
cannot escalate the peer past the parent session's limit. The executor requires
the resolved mode to be present in `session.modes.availableModes` and applies it
with `session/set_mode` before the first prompt, because the agent's own
`defaultMode` was measured to let a Write execute with no permission request
emitted at all. When the agent offers no option matching the user's outcome the
executor denies rather than falling back to the first offered option — approving
"proceed once" against `[allow_always, reject_once]` must not answer
`allow_always`. `optionKindForOutcome` is exhaustive over the enum with a `never`
check and a runtime default that denies, so an unmapped outcome cannot become a
grant. A host-policy denial (headless, permission avoidance, a display-only
TOOL_WAITING_APPROVAL listener, or a routed interactive question) rejects the
TOOL by selecting a `reject_once` option rather than answering ACP `cancelled`,
because `cancelled` tells a conforming peer the whole turn is over and would
abort the delegation on its first sensitive tool; it falls back to `cancelled`
only when the peer offered no reject option, and the stream-json responder path
still answers normally. An explicit user rejection of a single tool — the
dialog's Cancel/Esc, which `selectPermissionOption` maps to undefined — likewise
selects the peer's `reject_once` option instead of answering `cancelled`, so
denying one action scopes to that tool and the turn continues, matching the
button's "suggest changes" promise and the in-process sibling (which records
"User did not allow tool call" and lets the model proceed) rather than abandoning
the remaining work and billing a fresh prompt to re-delegate.

Child-process handling: the child is spawned detached with `sanitizeChildEnv`,
because `executor.command` comes from a project-level file, so a repository the
user merely cloned picks the executable and must not receive the daemon bearer
token. The handshake races a 10s deadline against both the `error` and `exit`
events, since a command that spawns and stays silent would otherwise hang
`create()` forever. Turn wall time defaults to 10 minutes when
`max_time_minutes` is omitted, matching the workflow subagent default, and an
invalid value throws before spawn. The child is owned by acp-bridge's
ProcessRegistry with `ownsProcessTree`, so disposal escalates SIGTERM to
SIGKILL and a SIGTERM-resistant descendant cannot survive; `dispose()` suppresses
only the two shapes where the foreign root is already gone — the signal it was
sent, and a root that exits on its own mid-snapshot (a Linux race where the
prompt rejects on stdout EOF before the process `exit` event, invalidating the
initial tree snapshot) — via the exported, unit-tested
`isExpectedExternalAgentCleanupExit`. The second shape covers the peer's own exit
status for ANY code/signal (`exited uncleanly during shutdown`), which acp-bridge
raises only after it has driven every owned process group to empty, so it reports
a foreign agent that exited — already reflected in `terminateMode` — not a cleanup
failure; letting `dispose()` reject on it would replace the turn's declared
terminal state (a user-CANCELLED turn would surface to the parent as `failed`).
Every genuine cleanup-PROOF failure (a truncated snapshot, an absent or
non-group-leader root, a failed snapshot/signal/inspect, or an exceeded exit
deadline — i.e. a descendant that may have survived) still propagates. A
post-handshake exit handler drains parked approvals;
its ERROR emit is guarded by `rawListeners`, because AgentEventType.ERROR is
Node's 'error' event and the background, resume and workflow emitters attach no
listener, so an unguarded emit would turn a child crash into an uncaught
exception. stderr goes through acp-bridge's `createStderrForwarder` for chunk
buffering and credential redaction. `methodNotFound` returns a real
`RequestError`, the only form the SDK preserves as -32601 rather than
repackaging as -32603, and the SDK's `extMethod` hook answers the same way
instead of hanging the agent.

Token usage for an external turn is unknown rather than free: the adapter
exposes only a context-window gauge, and feeding a level into accumulating
statistics would inflate totals past the window. The Agent tool therefore
suppresses execution stats for external subagents, skips live and completion
stat refresh, reports no execution summary, and appends "[External executor
token usage and cost are unavailable.]" to the result. That notice is a suffix
appended after the model-visible text and its empty-text fallbacks on both the
foreground and background completion paths, never baked into the text itself —
baking it in would make a non-GOAL background run that produced no text publish
the notice in place of the real failure reason (`Agent terminated with mode:
TIMEOUT`), because the `finalText || <reason>` fallback would see a non-empty
string. Transcript metadata records `executor: 'acp'` and no `persistedCliFlags`,
so the provenance is inspectable and the resume denial above has something to key
on. Stop reasons are mapped narrowly — end_turn to GOAL, max_turn_requests to
MAX_TURNS, cancelled to CANCELLED, and refusal, max_tokens and anything unknown
to ERROR — so a truncated external turn cannot be reported as completed, and a
peer that *refused* the task is not relabelled as a user cancel (CANCELLED
renders "cancelled by the user", a false statement that also hides the refusal
from telemetry). A wall-time expiry is classified by the timer's own
`ExternalAgentTimeoutError` identity rather than re-reading `Date.now()` in the
catch, because a second realtime clock can miss the monotonic deadline by under a
millisecond and misreport a turn that merely reached its budget as ERROR instead
of TIMEOUT. The continuation loop also re-checks that budget before dispatching
each round: `remaining` clamps to 0 once the wall time is spent, and without a
guard `connection.prompt(...)` is evaluated first — a new, billed model turn
really reaches the peer — one tick before `wait`'s 0ms timer rejects with
`ExternalAgentTimeoutError`, so the catch disposes (SIGTERM/SIGKILL) a peer that
just started work and the message the round already drained and recorded as
delivered is never processed. The loop now sets `TIMEOUT` and breaks before that
dispatch when `remaining <= 0`, mirroring the in-process sibling, which checks
the budget at the top of every round; it sets `terminateMode` directly rather
than throwing, because the catch classifies wall-time expiry by error identity.
And a continuation `execute()` turn — a resident external agent
re-invoked per incoming user message — emits the task as a user-side
`EXTERNAL_MESSAGE`, matching the in-process sibling, so the JSONL transcript does
not lose every message after the first. When a turn ends with a tool call still
open (cancel sets `cancelled` before the peer's terminal `tool_call_update` can
arrive, and timeout or peer crash leave it mid-flight), `runTurn`'s finally
flushes every entry left in the tool map as a failed `TOOL_RESULT` +
`TOOL_RESPONSES_FINALIZED` before `FINISH`, so the inline frame and Web Shell row
stop showing a tool executing forever, the JSONL `functionCall` is paired with a
`tool_result`, and the `FINISH` totals sum — the in-process sibling closes the
same gap via `onAllToolCallsComplete`. The session-update entry point drops any
update once the turn is over for ANY reason — its guard now also checks
`disposed` and the integrity-abort signal, not just `cancelled` — because on the
timeout and crash paths the catch awaits `dispose()` before the finally clears
`executing` and `cancelled` stays false, so a peer still writing would otherwise
be appended to `finalText`, emitted as `STREAM_TEXT`/`ROUND_TEXT`, and counted
into the `FINISH` totals for up to `EXIT_DEADLINE_MS` after termination, handing
a partial result to the parent verbatim on the non-rethrowing TIMEOUT path.
Safe mode also refuses an external executor at the dispatch gate, not only by
filtering discovery: `loadSubagent` resolves a `subagent_type` from disk
regardless of safe mode, and a cloned repo can ship `.qwen/agents/<builtin>.md`
carrying an `executor` that resolves before the built-in of the same name, so
safe mode now throws rather than spawn a definition-supplied binary.

The design doc now describes only the shipped product and these fail-loud
rules; the rejected peer-backend alternative is recorded as rejected.

Verification: core and cli `tsc` 0 errors, ESLint and Prettier clean, `git diff
--check` clean, cli executor tests 48/48, core subagent/agent/runtime suites
1483/1483, cross-package contracts and the two core-export sync guards 17/17.
Mutation
proofs, each run in both directions: removing the frontmatter executor guard
fails 8 tests, removing the workflow `agent()` rejection fails 6, replacing the
deny fallback with the first offered option fails 3, returning a plain Error
instead of `RequestError` fails the -32601 wire assertion, narrowing
`isExpectedExternalAgentCleanupExit` back to the signal-only form fails the
cleanup-exit classification test (which now also pins tolerating the peer's own
non-zero exit code), answering a host-policy denial with `cancelled`
instead of a `reject_once` selection fails 5, deriving the peer mode from the raw
definition instead of the host-resolved policy fails its dispatch test, reading
the sanitized frontmatter value instead of the original YAML node fails the
null-args rejection, making the `document.errors` refusal unconditional (instead
of executor-scoped) fails the non-executor lenient-load test, un-anchoring the
executor-claim text probe (dropping the column-0 / optional-quote form) fails
both the quoted-key refusal and the nested-key lenient-load tests, disabling the
turn-end `flushOpenTools` fails the open-tool flush test, deleting the
`loadCliConfig` executor registration fails the registration test, narrowing the
session-update guard back to `cancelled`-only lets a post-timeout `LATE` chunk
into `finalText` (failing the post-terminal-drop test), and disabling the
safe-mode dispatch refusal lets an external executor run in a trusted safe-mode
folder (failing the safe-mode refusal test), routing a user's Cancel back to ACP
`cancelled` (dropping the `reject_once` fallback at the respond site) fails the
user-cancel denial test, and reverting the executor-error guard to fire on any
`document.errors` entry instead of scoping to errors that reach the executor line
fails the unrelated-error-before-executor load test while the quoted-key and
unterminated-quote refusals stay green, removing the continuation budget guard
lets a 0-`remaining` round dispatch a second prompt (failing the
no-dispatch-after-budget test, which then sees two prompts and GOAL instead of
one and TIMEOUT), and reverting `astLostExecutor` to also require
`frontmatter.executor === undefined` lets a `parseDocument`-dropped executor load
through the `parseSimple` fallback as `command: '|'` (failing the dropped-node
refusal test), and re-throwing the raw error from the `toJS()` wrapper instead of
the graceful `SubagentError` fails the unresolved-alias refusal test (the
rejection becomes the raw "Unresolved alias" YAML error, not /invalid executor
block/). Mapping
`refusal` back to CANCELLED fails the continuation test, and dropping the
continuation `EXTERNAL_MESSAGE` emit fails the transcript test. The wall-time
test was the flake the timeout-classification fix removes; it now passes 5/5
deterministic runs. The real-subprocess suite is gated
`describe.skipIf(process.platform === 'win32')` like the repo's other
real-process suites, because Windows tree-kill reports a numeric exit code that
`dispose()` surfaces as unclean — the `test_windows` lane is merge-queue-only, so
an ungated suite there would eject the entry. The executor tests
drive a real ACP child process speaking the wire protocol, across
init/session/mode/prompt hangs and exits, max_tokens and unknown stop reasons,
duplicate permission callIds and optionIds, ask-user denial, descendant tree
kill, env sanitization and unsupported-extension responses. A separate
end-to-end run drove the real `@agentclientprotocol/claude-agent-acp` adapter and
a real model through this executor: `session/set_mode` `default` landed before
the prompt despite conflicting factory inputs, one permission request was emitted
and answered, a `Cancel` produced no file while a `ProceedOnce` selected the
adapter's `allow-once` (not its broader `allow-always`) and wrote exactly the
requested content, and both turns reported zero tokens — which is why the Agent
tool suppresses the summary rather than presenting zeros as free.

Not done: the real-adapter run above is a direct-factory harness, not the full
settings-loader → CLI → Web Shell path, and it does not exercise a browser
approval dialog, a second permission request in one turn, or headless auto-denial
against the real adapter; only macOS has been exercised, so the Windows `.cmd`
spawn path is unimplemented and untested (R3-1, left open — `cross-spawn`, the
repo's safe Windows launcher, is not a `packages/cli` dependency, and `shell:
true` around a project-supplied command is an injection surface, so this needs a
dependency decision and a Windows-testable design rather than a guess); the
optional mid-turn external-message waiter is unimplemented (R3-6, left open —
queued messages are still drained between turns, but an external subagent cannot
park for a not-yet-arrived one); and the Web Shell approval dialog still uses the
`info` confirmation variant, so it renders no file diff and has no screenshot
here. `ROUND_START`/`ROUND_END`/`USAGE_METADATA` are still not emitted, but the
one Critical consumer — the workflow stall watchdog, which arms only on those
events — can no longer be reached by an external agent at all, because workflow
`agent()` rejects external-executor definitions before spawn; `USAGE_METADATA`
also has no honest source (the adapter exposes a context-window gauge, not
per-turn token deltas). The round-4 through round-7 fixes in this commit were
audited to convergence by mutation proof and direct full-diff review; the earlier
rounds' agent-driven directionless audit did not converge because those agents
died on infrastructure failures.

* fix(subagent): close the round-10 review Criticals on the external executor

Five Criticals from the round-10 review of the external-agent executor and its
definition loader. Each is mutation-verified (reverting the fix turns its test
red) and the surrounding guarantees are pinned.

R10-1 — the executor-frontmatter parser derived both the claim and the value
from a column-0 regex plus the lenient `parseSimple` fallback instead of the
real YAML AST, which got the verdict wrong in both directions. Under-detection:
a TAB-indented top-level `executor:` is invalid YAML the AST drops, and a
column-0 probe missed it, so the definition loaded executor-less and ran
in-process under a Qwen model with nothing on any channel. Invented dispatch:
`parseSimple` hoists an `executor:` line sitting inside a `description: |` block
scalar into a top-level key, so a file declaring no executor loaded as external
and dispatched a command that exists only as prose. Both legs now come from the
AST: the claim probe is indentation-tolerant (`^[ \t]*`), `claimsExecutor` no
longer reads `frontmatter['executor']`, and `executorRaw` is `undefined` (never
the `parseSimple` value) when `parseDocument` has no top-level executor node.
The accepted cost — an `executor:` token nested under another key in an
otherwise-malformed file is now refused — is a visible, user-fixable
over-refusal, which beats an invisible substitution or an invented dispatch.

R10-2 — the load-time executor refusals were file *skips*, so `loadSubagent`'s
session>project>user>extension>builtin fall-through substituted a
lower-precedence in-process definition (or a case-insensitive builtin) of the
same name and the delegated turn ran under a Qwen model — the exact substitution
this feature prevents, with only a discovery-time `console.warn`. Executor-block
refusals are now recorded by declared name during the level scan (the
`parseSubagentContent` re-wrap preserves the `SubagentError` and its
`subagentName`), and the by-name fall-through path throws the recorded refusal
before descending or reaching `getBuiltinAgent`. Scoped to executor refusals (not
parse failures generally) so an arbitrary malformed repo file cannot disable an
unrelated builtin, and scoped to the no-level fall-through path (the explicit
-level path does not fall through, so it cannot substitute) to keep the
management callers' contract unchanged; `isNameAvailable` treats a refusal as
"name taken".

R10-3 — `isExpectedExternalAgentCleanupExit` swallowed acp-bridge's
`exited before its initial process-tree snapshot completed`, which
`mergeAsynchronousSnapshot` records and returns BEFORE `collectOwnership`, so a
detached (`setsid`) descendant was never enumerated, signalled or reaped — the
tree was not proven gone, yet `dispose()` resolved silently and the parent saw a
clean teardown. That shape is now `isUnprovenExternalAgentTreeExit`: `dispose()`
reports it (`debugLogger.warn` + an `AgentEventType.ERROR`) and resolves rather
than rethrowing (it is awaited between terminal-state classification and return,
so rethrowing would convert a classified TIMEOUT/CANCELLED into a thrown ERROR).
`isExpectedExternalAgentCleanupExit` is narrowed to the peer's own unclean exit
(raised only once the tree is proven gone); every other cleanup-proof failure
still propagates.

R10-4 — `runTurn` destructively drained the external-input queue and recorded the
messages as delivered before the round-top budget check, so a wall-time expiry
(or a bottom-of-loop abort) discarded user messages already emitted as delivered
— the transcript certified delivery of a message never sent, and the registry had
already spliced it out. The drain is now gated on the budget and the abort signal
first, mirroring the in-process sibling (`agent-core` checks the budget before
`drainExternalInputs`): when the budget is spent or the signal aborted, the loop
breaks before draining, so the message stays queued for a future turn.

R10-5 — an absent `max_time_minutes` silently meant "10 minutes then terminate"
for an external agent, while meaning "no cap" everywhere else, so the same
definition behaved differently based only on whether it declared an executor, and
a TIMEOUT turn (handed to the parent as the answer with no truncation marker) was
reachable by default. The cited rationale was void: `DEFAULT_WORKFLOW_SUBAGENT_
MAX_TIME_MINUTES` is read only on the workflow path, which hard-rejects external
executors upstream, so it never reaches this executor. An absent value is now
passed through as `undefined` (no timer), matching `agent-core`; a caller that
wants a cap supplies `max_time_minutes` (validated positive, finite, ≤ the Node
timer range).

Verification: core and cli `tsc` 0 errors (after a full `npm run build` to refresh
the workspace `dist` the merge had left stale); ESLint and Prettier clean; `git
diff --check` clean; cli executor suite 50/50; core subagent/agent/runtime
suites 2510 passed (6 skipped); cross-package contracts and the two core-export
sync guards 17/17. Mutation proofs, each run in both directions: reinstating
`?? 10` fails the no-default-cap test; removing the pre-drain budget/abort gate
fails the queue-preserved test (the message is drained, recorded delivered, and
lost); making `dispose()` swallow the snapshot-race error fails the
unproven-tree-report test; reverting the claim probe to column-0 anchoring fails
the nested-token refusal test; restoring the `parseSimple` fallback for
`executorRaw` fails the block-scalar-prose test (it loads `command:'npx'` from
prose); and no-oping the recorded-refusal throw fails the by-name dispatch test
(it resolves the builtin instead of rejecting). Reverse-audit converged after one
fix (scoping the R10-2 throw to the fall-through path so the explicit-level
management callers keep their null contract), then clean passes over the
production and test diffs.

* fix(subagent): close the round-11 review Criticals on the external executor

Six Criticals from the round-11 review: five on the executor and its definition
loader, one a regression the round-10 R10-2 fix itself introduced. Each is
mutation-verified (reverting the fix turns its test red).

R11-1 — a file that CLAIMS an executor but fails an EARLIER validation (a
missing description, a bad approvalMode) was skipped with nothing recorded, so
loadSubagent's fall-through silently substituted the builtin of the same name.
The executor claim and the trusted declared name are now computed at function
scope, right after the frontmatter is parsed (BEFORE any validation can throw),
so the catch converts ANY load failure of an executor-claiming file into a named
executor refusal the fall-through throws. (A file whose own name is unparseable
stays undefined-keyed and falls through to the generic wrap — it cannot be
matched by name anyway.)

R11-2 — the wall-time budget was computed per-turn (timeoutMs - elapsed this
turn), so a resident agent re-invoked per incoming message (resetStats:false,
which preserves durationMs) got a FRESH max_time_minutes cap each continuation
and could overrun the whole-delegation budget indefinitely. Both remaining
computations now subtract the accumulated durationMs, matching the in-process
sibling's preserveStats base: max_time_minutes caps the whole delegation. Both
production continuation callers (background-agent-resume, agent) pass
resetStats:false, so the cumulative cap reaches the real path.

R11-3 — the host approval mode was used AS the peer's mode id, but the
vocabularies differ (host/qwen: auto-edit/auto/yolo; Claude: acceptEdits/
bypassPermissions), so a host auto-edit could select a peer mode that does not
exist (or, worse, a qwen peer's broader auto). resolvePermissionMode now maps the
host policy to a peer-vocabulary-independent TOKEN (default/plan/acceptEdits/
bypass) and connect() picks the first id the peer actually advertises via a
canonical-first alias table, refusing (naming the policy) when none is
advertised. The mapping never WIDENS: acceptEdits prefers the peer's narrowest
edit-only alias.

R11-4 — the executor refusal was keyed by the lenient parseSimple name, which
strips only double quotes, so a single-quoted `name: 'Explore'` was recorded
under "'explore'" and missed the 'explore' dispatch lookup, falling through to
the builtin. The refusal is now keyed by the real YAML AST's name (parseDocument
strips both quote styles), with the lenient value only as a fallback when the
AST read throws.

R11-5 — the approval confirmation's prompt rendered the OPTION LABELS (which the
dialog already shows as buttons) and dropped the action's arguments, so the user
approved "Write" without seeing the `rm -rf ./build` it would run. The prompt is
now describeExternalAction(toolCall): the title plus the rawInput, bounded to
300 chars and stripped of C0/C1/DEL control chars (foreign-process data, never
rendered as markup).

R10-2 (fix-induced) — extension agents load via loadSubagentFromDir, which skips
+ warns on a refusal, so the R10-2 level-scan recording never ran for them and
the extension leg of the fall-through read an empty map. loadSubagentFromDir now
takes an optional refusal collector; the runtime loadExtension records each
extension's executor refusals onto extension.agentExecutorRefusals, and
listSubagentsAtLevel('extension') merges them into the 'extension' refusal
bucket before findSubagentByNameAtLevel returns, so the by-name fall-through
refuses them too. (The install/update consent path also calls
loadSubagentFromDir, but only for display — it does not feed the dispatch
fall-through, so it needs no collector.)

Verification: core and cli tsc 0 errors; ESLint and Prettier clean; cli executor
suite 57/57; core subagent-manager suite 199/199; core subagent/agent/runtime
regression net 2513 passed (6 skipped). Mutation proofs: dropping the cumulative
durationMs subtraction fails R11-2 (turn 2 ends GOAL, not TIMEOUT); removing the
peer-canonical acceptEdits alias fails R11-3 (the auto-edit dispatch finds no
advertised mode); reverting the approval prompt to the title fails R11-5 (no
`rm -rf ./build`); neutralizing the catch conversion fails R11-1 (loadSubagent
resolves the builtin); keying by the lenient name fails R11-4 (single-quoted
name resolves the builtin); and no-oping the extension-refusal merge fails the
R10-2 extension leg. Reverse-audit converged after one fix (the R11-2 test burned
the whole budget in turn 1, tripping the R10-4 pre-drain gate's GOAL-to-TIMEOUT
reclassification; redesigned so turn 1 ends GOAL and turn 2 hits the cumulative
cap), then clean passes over the production and test diffs.

* fix(subagent): close the round-12 review Criticals on the external executor

Six Criticals from the round-12 review — three on the executor, one a
regression the R11-5 fix itself introduced, and two on the definition loader's
refusal bookkeeping. Each is mutation-verified (reverting the fix turns its
test red).

R12-1 — execute() re-rendered and re-sent the entire system-prompt bundle on
every continuation turn, although the live ACP session has held it since turn 1
(NewSessionRequest has no system-prompt channel, so the prompt content block is
the bundle's only channel and the FIRST turn must still send it). A resident
external agent re-invoked per incoming message re-sent the definition's
systemPrompt + the appended rules + the whole memory hierarchy (~16.5 KB) every
turn, billed by the peer and pushing its context toward compaction, while the
transcript recorded only the task. The bundle is now gated on the first turn:
`const continuation = this.started` is hoisted above the render and
continuations send only the task.

R12-2 — the turn-entry `emitInputs` recorded the caller's messages as delivered
BEFORE the round-top wall-time guard, so an over-budget continuation wrote
EXTERNAL_MESSAGE user records for a prompt that was never dispatched (the guard
breaks TIMEOUT first). The entry emit moved to the loop's commit point — after
the budget guard, before the prompt — keyed on the ENTRY round (`entryRound`,
not `this.round === 1`, which is broken under resetStats:false because the round
counter is preserved across continuations). Emitting before the dispatch keeps
user-before-assistant order in the JSONL transcript.

R11-5 (fix-induced) — the R11-5 confirmation carried the peer's arguments but
rendered them as MARKDOWN (the `confirmationDetails` omitted
`renderPromptAsPlainText`), so a glob `**` was eaten and a `[label](url)` could
mis-render, misrepresenting the action being approved. The info variant now sets
`renderPromptAsPlainText: true` (the bound and control-strip stay — the flag is
not a substitute for them). Consolidating the local control-char stripper into
the centralized `stripAnsiAndControl` was considered but requires a new
cross-package core export; the flag is what neutralizes the rendering, and the
local stripper already kills the ESC byte so no escape sequence survives.

R12-3 — `resolveResumeTarget` reaches the `loadSubagent` that R10-2/R11 made
throwing, unguarded, so background-agent discovery swallowed the throw into a
debug-only per-sidecar warning and the recovered row VANISHED from /tasks
instead of listing with a `resumeBlockedReason`. The `loadSubagent` call is now
wrapped and the throw converted into the existing `unavailableReason` shape
(both call sites already fold it into `resumeBlockedReason`). The
executor-provenance early return stays ahead of the call, so the four
cold-external-resume provenance legs still refuse before any load.

R12-4 — `executorRefusals` was only rewritten when a level scan reached the end
of its `try`, so a scan that could not read the directory left the previous
scan's refusals in place and `loadSubagent` kept throwing for a file that no
longer exists. Both non-scan paths now reset the level's map: the `readdir`
catch and the `project == home` early return.

R12-5 — the indentation-tolerant raw-text probe made `claimsExecutor` true for
frontmatter that only MENTIONS `executor:` as prose inside a `description: |`/`>`
block scalar, so `astLostExecutor` hard-refused an in-process definition that
carried an unrelated tolerated YAML quirk (duplicate key, tab indent), and keyed
the refusal by its name — blocking every lower-precedence definition and builtin
of that name. The probe match is now excluded when its offset lies inside a
block scalar (walked via the real AST's BLOCK_LITERAL/BLOCK_FOLDED scalar
ranges, fail-closed if the walk throws). A genuine top-level — even
TAB-misindented or nested — `executor:` key is not inside a block scalar, so the
R10-1/R9-2/R7-1 refusals all still hold.

Verification: core and cli tsc 0 errors; ESLint and Prettier clean; cli executor
suite 61/61; core subagent-manager 202/202 and background-agent-resume 56/56;
core subagent/agent/runtime regression net 2517 passed (6 skipped). Mutation
proofs: re-sending the bundle on continuation fails R12-1 (turn 2 carries 2
blocks); moving the entry emit back above the guard fails R12-2 (`delivered`
becomes `['second']`); dropping `renderPromptAsPlainText` fails the R11-5 case;
rethrowing in `resolveResumeTarget` fails R12-3 (the row vanishes); deleting the
catch reset fails R12-4 (stale refusal persists); dropping the block-scalar
exclusion fails both R12-5 variants (they reject). Reverse-audit converged to
clean passes over the production and test diffs.

* fix(subagent): resolve the two carried Criticals via maintainer decision

Closes the two Criticals standing since round 3, per the maintainer's decision
on the round-12 review: both are resolved by an explicit, surfaced limitation
rather than an untestable/billing-risky implementation. Each is
mutation-verified (reverting the fix turns its test red).

R3-1/R3-22 (Windows spawn) — decision: POSIX-only for this release. On Windows
an npm-installed adapter resolves to a `.cmd` launcher that libuv's PATH search
never finds (bare name + `.exe` only) and Node >= 18.20.2 refuses to spawn
without a shell; the process-tree reaping is likewise POSIX-specific. The
executor previously failed at dispatch with a misleading `spawn <cmd> ENOENT`
(reporting an installed adapter as missing). A new
`assertExternalAgentSpawnPlatformSupported` guard now fails closed at the top of
`create()` with a clear, actionable "POSIX-only in this release" error, before
any spawn. A cross-spawn-style PATHEXT resolution + quoted `cmd.exe` arm is a
tracked follow-up (it changes the security-critical detached-spawn path and is
untestable without a Windows lane).

R3-6/R3-51 (mid-turn waiter) — decision: explicitly decline + surface the
limitation. ACP v1 has no mid-turn injection primitive (no `session/steer`), so
the executor deliberately does NOT implement `setExternalMessageWaiter` /
`setExternalMessageWaitPredicate` (documented on the class); input arriving
mid-prompt is delivered at the next turn boundary via the provider. To stop the
background loop presenting a queued steer as if delivered mid-turn, the
delegation result for an external executor now appends
`EXTERNAL_MID_TURN_INPUT_NOTICE` (alongside the existing usage notice) stating
queued input is delivered between turns, not mid-turn. True mid-turn steering
(cancel + re-prompt) re-bills the in-flight turn — a protocol/billing decision,
tracked as a follow-up.

Verification: cli + core tsc 0 errors; ESLint + Prettier clean; cli executor
suite 63/63 (incl. a win32-stubbed `create()` rejection that never reaches
spawn); core agent suite 288/288; core subagent/agent/runtime regression net
2518 passed (6 skipped). Mutation proofs: removing the win32 throw fails both
R3-1 tests; dropping the mid-turn notice fails the R3-6 result assertion.
Reverse-audit converged to clean passes over the production and test diffs.

---------

Co-authored-by: probe <probe@local>
2026-09-10 03:13:05 +00:00
qqqys
35a702c330
fix(dws): retry completed response delivery (#11387)
* fix(dws): retry completed response delivery

* fix(dws): harden completed response retries

* fix(dws): preserve authorized response retries

* test(dws): cover completed reply delivery

* fix(dws): separate local delivery deferrals

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
2026-09-10 02:36:34 +00:00
callmeYe
471b6e5ef5
feat(web-shell): add unified session sources (#11262)
* docs(web-shell): design session source registration

* docs(web-shell): add Chinese session sources design

* feat(web-shell): add unified session sources

* fix: complete session source lifecycle and UI contracts

* fix(core): keep source registration in top-level sessions

* fix(live-host): keep packaging in the standalone npm workspace

* fix(web-shell): capture reading anchors before admitting history pages

* fix(web-shell): close source review gaps and restore regression coverage

* fix(web-shell): preserve standalone source links and close hidden dialogs

* fix(web-shell): preview session-owned sources without workspace access

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-09-10 02:34:58 +00:00
Dinesh Palanisamy
69db15e234
fix(core): preserve every reasoning episode's signature during history consolidation (#8260)
Some checks failed
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Lint & Static (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / web-shell E2E Smoke (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (no-AK, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (CLI, No Sandbox) (push) Blocked by required conditions
Qwen Code CI / Desktop Shell (ubuntu-22.04) (push) Blocked by required conditions
Qwen Code CI / Desktop Shell (windows-2022) (push) Blocked by required conditions
E2E Tests / Build for E2E (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/1 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 1/2 (push) Blocked by required conditions
E2E Tests / E2E Test - macOS - shard 2/2 (push) Blocked by required conditions
E2E Tests / E2E Interactive - OpenTUI renderer (bun) (push) Blocked by required conditions
E2E Tests / channel-plugin E2E (nightly) (push) Blocked by required conditions
E2E Tests / cron-interactive E2E (nightly) (push) Blocked by required conditions
SDK Java / macos-latest / Java 21 (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
SDK Python / Classify PR (push) Has been cancelled
SDK Python / SDK Python (3.10) (push) Has been cancelled
SDK Python / SDK Python (3.11) (push) Has been cancelled
SDK Python / SDK Python (3.12) (push) Has been cancelled
* fix(core): preserve every reasoning episode's signature during history consolidation

geminiChat.ts's turn-consolidation step merged every thought-flagged
part in a turn into a single blob and kept only the first
thoughtSignature it saw. A turn with more than one distinct reasoning
episode -- e.g. one reasoning span per parallel tool call on Anthropic
interleaved thinking or OpenAI Responses reasoning items -- silently
lost every signature after the first, and hoisted the merged blob to
the front of the turn regardless of where the episodes actually
occurred relative to the tool calls.

Replace the merge-all/keep-first-signature pass with a single-pass
algorithm that treats a text-less, signature-only chunk as the natural
end of an episode on both wires, closes ("flushes") the current
episode when a non-thought part appears or when a thought part carries
fresh text while the open episode already has both text and a
signature, and concatenates (not "keeps only the first") text and
signature fragments within an open episode so a signature split across
multiple stream chunks is reassembled correctly. Each episode is now
preserved as its own history Part in its original position.

The XML-tool-call-recovery fallback (#8003) had to be updated to match:
it previously identified "text parts to remove and replace with
remainingText" via a bare `.text !== undefined` check, which also
matched a reasoning episode Part (flushThoughtEpisode always sets
`episodePart.text`, even '' for a signature-only episode) -- silently
deleting the turn's reasoning episode, text and signature both,
whenever XML recovery fired on a turn that also carried one. Switched
to isValidNonThoughtTextPart, matching this path's actual intent.

mergeConsecutiveAssistantMessages in anthropicContentGenerator's
converter.ts had a related bug: it unconditionally hoisted all
thinking blocks to the front when merging two adjacent assistant
messages, which would silently undo the primary fix's chronological
ordering the moment two assistant messages needed merging. Since
interleaved-thinking-2025-05-14 is unconditionally enabled, thinking
blocks no longer need to lead; changed to straight concatenation.

Fixes #8258.

* fix(anthropic): keep manual-mode thinking history leading with thinking

Addresses review feedback on #8260: mergeConsecutiveAssistantMessages's
straight concatenation preserves chronological order but can leave a
merged assistant turn's content beginning with text instead of
thinking (e.g. [text A] + [thinking B, tool_use B] -> [text A,
thinking B, tool_use B]). Anthropic's manual (non-adaptive)
extended-thinking contract requires the final assistant turn of a
thinking-enabled request to begin with a thinking block whenever a
tool_use remains in it; adaptive thinking has no such requirement, so
the request would 400 on the follow-up tool-result turn.

Add ensureLeadingAssistantThinking, a converter option gated on the
outgoing request's actual thinking.type === 'enabled' mode (passed
from anthropicContentGenerator.ts). When set, after all merge/cleanup
passes finish, it relocates the most recent assistant message's first
contiguous thinking/redacted_thinking run to the front of its content
array -- and only that run, leaving every other block (including
later thinking blocks and their relative order) untouched. It does
not fabricate a thinking block where none exists, and is a no-op for
adaptive-thinking models (Opus 4.7+, every 5.x) and thinking-off
requests.

Replaces the previous "pins current behavior" regression test (which
documented the residual risk without fixing it) with two assertions:
adaptive/default mode still preserves chronological order, and the
new option produces the required leading-thinking shape. Adds a
generator-level regression test using an explicit-budget (manual)
configuration on claude-opus-4-6, covering the full tool-loop request
shape, the interleaved-thinking beta, and signature preservation.

* fix(core): align XML-recovery text predicates to stop leaking raw XML

Addresses a Critical from PR #8260 review (and the related "three
uncoordinated predicates" Suggestion it escalated): contentText's
filter (`part.text && !part.thought`) and the XML-recovery removal
loop's filter (`isValidNonThoughtTextPart`, which additionally rejects
any part carrying `thoughtSignature`) disagreed on what counts as
"visible text." A part with `thoughtSignature` set but no `thought:
true` -- a real wire shape (loggingContentGenerator.ts's stream
aggregation spreads `thought` and `thoughtSignature` independently) --
was picked up by contentText for XML detection but survived the
removal loop untouched: recovery fired, but the raw `<invoke>` XML was
never stripped, leaking it into durable history duplicated alongside
the recovered functionCall.

Introduce a single `isVisibleTextPart` predicate (`Boolean(part.text)
&& !part.thought`) shared by contentText's initial computation, its
post-recovery recompute, and the removal loop's textIndices scan.
Deliberately the looser of the two prior predicates, not the stricter
one: narrowing contentText itself to exclude thoughtSignature-bearing
text would make `hasAnyContent` treat genuine visible text as absent,
throwing "Model stream ended with empty response text" on ordinary
turns. flushThoughtEpisode always sets `thought: true` on episode
parts, so `!part.thought` alone (already contentText's semantics)
already protects reasoning episodes from the removal loop without
isValidNonThoughtTextPart's stricter signature exclusion.

Adds a regression test that reproduces the leak on unfixed code
(confirmed failing before this fix, passing after) with a plain-text
part carrying a stray thoughtSignature and XML content.

Also addresses two outstanding test-coverage Suggestions from the same
review round:
- converter.test.ts: a multi-thinking-run case for
  ensureLeadingAssistantThinking, guarding the "only the first run
  moves" invariant against a hoist-all-thinking mutant that the
  existing single-run test couldn't catch.
- anthropicContentGenerator.test.ts: a generator-level adaptive-mode
  test mirroring the manual-mode one, guarding the `thinking?.type ===
  'enabled'` gate against a `!!thinking` regression that would
  reintroduce the hoist-every-thinking corruption on adaptive models.
- geminiChat.test.ts: asserts the interleaved-episode test's recorded
  JSONL turn (not just in-memory history) preserves both reasoning
  episodes and their signatures, guarding --resume fidelity against a
  recording-only regression that in-memory assertions can't see.

* fix(core): drop dangling unsigned thought episode on truncated tool turns

Addresses the Critical from PR #8260's latest review round, verified by
tracing the actual code before fixing (not taken on the reviewer's
word): flushThoughtEpisode's own "Known limitation" note already
acknowledged that a stream cut off before an episode's terminating
signature-only chunk arrives (SSE drop, MAX_TOKENS) leaves that
episode unsigned. Left in history alongside a tool_use in the SAME
turn, this permanently wedges a session: once the tool result is
appended, the turn enters dropUnsignedThinkingFromAssistantMessages's
"active tool-use chain" and every subsequent request throws on
proxy-hosted adaptive Claude (native Anthropic rejects the unsigned
block itself instead) -- neither is recoverable without editing
history out-of-band, since the malformed turn is now a permanent part
of the session's history.

Fix: after the trailing flushThoughtEpisode() call, if the turn has a
tool_use and the last consolidated part is an unsigned trailing
thought episode, drop it before it can ever reach history. Scoped to
`hasToolCall` because a dangling unsigned episode with no tool_use in
the same turn is already filtered out safely downstream (it never
enters the active-chain path). Added a regression test reproducing the
exact wedge scenario, confirmed failing on unfixed code (the unsigned
episode survived in history) and passing after.

Also addresses four Suggestions from the same review round, each
verified against the actual code (one live-mutated to confirm it
catches what's claimed) rather than accepted at face value:
- converter.ts: added a test with two non-consecutive assistant
  messages (separated by a user turn) to discriminate
  ensureLeadingAssistantThinking's backward scan from a forward-scan
  mutation that reorders the wrong turn -- confirmed by temporarily
  applying the mutation and observing exactly this new test fail.
- converter.ts: added a multi-block first-thinking-run test to
  discriminate the run-extension loop from a `runEnd = runStart + 1`
  mutation that would split a multi-block run apart -- confirmed the
  same way.
- geminiChat.ts: added two tests for the episode-split condition's
  `openEpisodeText.length > 0` and `openEpisodeSignature !== ''`
  clauses (signature arriving before any text; multiple text deltas
  within one still-open episode, the normal live-streaming shape) --
  each confirmed to fail when its corresponding clause is removed.
- geminiChat.ts / converter.ts: fixed a stale comment (the JSONL
  recording no longer reads the recomputed contentText, it reads
  consolidatedHistoryParts directly) and qualified
  dropEmptyTextThinkingBlocks's doc, which read as contradicting
  flushThoughtEpisode's "still potentially replayable" rationale for
  the same empty-text+signature shape -- clarified that the two are
  consistent (disposability of non-latest-turn thinking, not
  invalidity of the shape itself), with a cross-reference each way.

One Suggestion from the same round was checked and found NOT to hold:
a claim that no test pins flushThoughtEpisode's "drop a whitespace-only,
signature-less episode" guard. Forcing that guard to unconditionally
true and running the full suite shows this is false -- "should
preserve text parts that stream in the same chunk as a thought" (an
existing test) goes red under exactly that mutation. No change made
for this one.

* fix(core): close recovery-coalescing and pipeline-ordering gaps in the dangling-episode fix

A scoped multi-model architectural review of the reasoning-episode
consolidation logic (3 independent reviewers, one per major model
family) converged on the same Critical finding, plus a second real
gap and a lower-priority structural one. Every finding was
independently re-verified against the actual code (traced by hand,
or confirmed/refuted via live mutation) before acting -- one line of
investigation that looked promising turned out to cause a real
regression and was redesigned rather than shipped as-is (see below).

Critical (corroborated by all 3 reviewers, verified by hand-tracing
the control flow myself): the per-stream trailing-pop fix from the
previous commit only inspects a single `processStreamResponse` call's
own output. The MAX_TOKENS *recovery* loop explicitly proceeds only
when the truncated turn has NO functionCall yet -- exactly the
precondition under which the per-stream check's `hasToolCall` is
false and never fires. If the recovery continuation then calls a
tool (an ordinary agentic-loop event), `coalesceRecoveryPairs` merges
the two attempts via `appendRecoveryContinuationParts`, whose dedup
anchor is blind to `thought` parts -- reintroducing the exact
permanent-wedge hazard the previous fix targeted, just via the
cross-request merge path instead of a single stream. Fixed by
re-running the same trailing-only check on the truncated turn's own
parts immediately before the merge, using "does the continuation
introduce a functionCall" as the `hasToolCall` signal.

A second reviewer-proposed fix (broadening the trailing-only check to
scan the whole parts array, to also catch an unsigned episode
immediately preceding a functionCall within a single stream) was
implemented, then REVERTED after the full test suite caught a real
regression: DeepSeek legitimately emits unsigned thinking blocks
right before a functionCall as its normal, complete wire shape
(DeepSeek doesn't validate thinking signatures the way Anthropic
does). A whole-array scan can't distinguish "truncated mid-episode"
from "a provider that doesn't sign its thinking" -- only the trailing
position can, since a stream's own truncation can only ever leave the
dangling episode trailing (anything that followed it in the same
stream would already have flushed it). Kept the check trailing-only
and added a test pinning this as accepted residual risk, matching the
code's own pre-existing "Known limitation" note on wire-protocol
non-compliance.

Lower-priority structural fix (found by one reviewer, verified by
reading the code myself): `dropEmptyTextThinkingBlocks` computes "the
latest assistant message" once, before `stripTrailingAssistantPrefill`
can later pop a genuinely-empty trailing message and promote an
earlier one to "new latest" -- stale index. Verified the trigger
conditions overlap in practice (`stripTrailingAssistantPrefill` is
gated on model version 4.6+; `ensureLeadingAssistantThinking` is
gated on manual/explicit-budget mode; both are true simultaneously
for exactly the "4.6+ model with an explicit budget_tokens override"
configuration this PR's own escape-hatch targets). Fixed by
reordering the pipeline to run `stripTrailingAssistantPrefill` before
`dropEmptyTextThinkingBlocks`, preserving `mergeConsecutiveUserMessages`'s
existing adjacency to `dropEmptyTextThinkingBlocks` so its own
cleanup invariant (fixing up newly-adjacent user messages after an
assistant message is dropped) is unaffected.

Every fix and every reverted attempt was verified against the full
`geminiChat.test.ts` + `converter.test.ts` + `anthropicContentGenerator.test.ts`
suites (540 tests, only the one pre-existing unrelated User-Agent
failure) and against targeted mutation testing: each new regression
test was confirmed to fail when its guarded code path is disabled or
reverted, and to pass once restored.

* fix(core): normalize leading thinking on every tool_use turn, not just the latest

Addresses the review round on #8260.

ensureLeadingThinkingOnLatestAssistantMessage repaired only the most
recent assistant message, which was wrong in two independent ways:

- #3786 describes the anthropic-compatible rejection against a PRIOR
  assistant turn carrying tool_use, and injectEmptyThinkingOnToolUseTurns
  correspondingly repairs every tool_use turn. Under latest-only scoping a
  turn normalized while it was current reverts to the text-leading shape on
  the next request, so the failure surfaces one turn after the turn that
  produced it.
- Keying the reorder on "is this the latest assistant message" made a
  turn's serialization depend on its position in history, so the same turn
  went out two different ways on consecutive requests. Since
  addCacheControlToMessages anchors its breakpoint on the last user
  message, that rewrote the cached prefix and forced a full prompt-cache
  re-read every turn.

Renamed to ensureLeadingThinkingOnToolUseAssistantMessages and gated on
tool_use, matching the option's own documented scope.

Also in this round:

- Apply dropDanglingUnsignedTrailingThought inside the XML tool-call
  recovery branch, before the recovered functionCall parts are appended.
  Recovery's gate requires hasToolCall === false, which is exactly when the
  per-stream drop early-returns, so an unsigned trailing episode survived
  and was then paired with a tool_use -- permanently wedging the session
  once the tool result returned.
- Drop the dead `.filter(part => part !== null)` in the recording path;
  redactStructuredOutputArgsForRecording only returns null for parts the
  enclosing ternary already excludes.
- Correct dropDanglingUnsignedTrailingThought's doc, which overclaimed that
  trailing-only scope distinguishes a truncated signing-provider episode
  from a non-signing provider's ordinary trailing thought. It does not; a
  truncated DeepSeek stream has the identical shape. Document the accepted
  false positive and the fact that the coalescing call site never reaches
  the JSONL record.
- Document the mirror-image episode limitation: two adjacent text-less
  signed thought parts concatenate their signatures into one part valid for
  neither block, newly reachable on the OpenAI Responses wire (#8169).

Tests: all four new behaviors are mutation-verified (fix reverted -> red,
restored -> green). 556/556 pass across geminiChat.test.ts and
anthropicContentGenerator/.

* fix(core): run the XML-recovery dangling-episode drop before remainingText is re-inserted

Addresses review round 3 finding R3-1 on #8260. The reviewer is right and
this is a hole in the previous round's own fix.

The third dropDanglingUnsignedTrailingThought call site was placed after
`recovery.remainingText` was spliced back into consolidatedHistoryParts.
When the dangling unsigned episode PRECEDES the consumed XML text part, the
re-inserted text lands behind the episode, so the trailing-only check sees a
text part last, no-ops, and the appended functionCall parts persist
`[thought(unsigned), text, functionCall]` -- an active tool-use turn holding
unsigned thinking, which makes
dropUnsignedThinkingFromAssistantMessages throw on every subsequent request.
Same permanent wedge the call site was added to prevent, reached by a
different shape.

The previous round only considered the episode-trailing case and treated a
preceding unsigned episode as the documented non-trailing residual risk.
That was wrong here: the episode IS trailing at the moment the consumed text
parts are spliced out, and only the re-insertion pushes it out of last
position.

Moved the drop into that window -- after the splice-out, before both the
re-insertion and the append -- which is the only point where a dangling
episode is guaranteed to be the last element. `insertAt` is now clamped
against the post-drop length, since the drop can shorten the array.

Regression test uses the trigger shape the reviewer named: an unsigned
episode followed by a plain-text part carrying a stray thoughtSignature and
no thought flag (the wire shape isVisibleTextPart's own doc calls out as
real), with non-empty remainingText so the ordering is observable. Confirmed
failing before this change and passing after.

575/575 across geminiChat.test.ts and anthropicContentGenerator/ on top of
the merged main; tsc, eslint and prettier clean.

* test(core): close the mutation escapes round 4 found in this PR's own tests

All six round-4 findings were Suggestions against tests added earlier in this
PR, and three were probe-verified mutation escapes -- guards that were
claimed as regression coverage but did not actually catch the regression
they named. Each fix below is verified by applying the mutation, observing
the new assertion go red, and restoring.

R4-3: the four XML-recovery episode tests asserted part presence only, never
order, even though episode-before-tool_use is the replay invariant the whole
PR exists to protect. Splicing recovery.functionCallParts ahead of the
episode previously left every one of them green. Added index-order
assertions to three of them; the mutation now fails 5 tests.

R4-1: the coalescing call site pinned only the drop direction, so an over-pop
localized there passed all 337 tests while the XML site's equivalent
mutation failed 2. Added the complementary test -- a SIGNED trailing episode
on the truncated turn survives when the continuation introduces a
functionCall. The over-pop mutation now fails it.

R4-6: two tests deliberately set a non-empty remainingText ('Sure.') to make
the drop-vs-reinsert ordering observable, then never asserted the text
survived. Skipping the re-insert previously left them green; it now fails 3
tests. This is user-visible prose that `--resume` would silently lose.

R4-2: the accepted-false-positive tripwire's own comment claims the reasoning
is gone "from history AND from the JSONL record", but only history was
asserted. That agreement is ordering-dependent -- recordArgs.message is built
from the already-dropped array -- so a future reorder placing the drop after
the record call would keep every assertion green while the JSONL retained the
wedge shape for --resume to rehydrate. Now asserts both surfaces.

R4-5: no fixture covered a first thinking run starting AFTER a tool_use
block, so the contract was pinned in neither direction. Current behavior
relocates it (the scan is a bare findIndex with no position condition).
Pinned, so a plausible "skip when the run starts after the first tool_use"
refinement can't silently ship the text-leading shape #3786 rejects.

R4-4: the canonical manual-mode fixture paired max_tokens: 500 with
budget_tokens: 42_000, violating Anthropic's documented
budget_tokens < max_tokens rule. buildSamplingParameters does not clamp
between them, so the fixture modelled a request the real API 400s on and
passed only because the client is mocked. Raised to 64_000.

Also widened the makeChunk test helper's part type to carry
thoughtSignature, which the new signed-episode fixture needs.

577 tests pass across geminiChat.test.ts and anthropicContentGenerator/;
tsc, eslint and prettier clean.

* test(core): pin the five unguarded branches round 5 found, and fix an unrealistic fixture

Round 5 reported six gaps in this PR's own tests. All six reproduce: in
each case the shipped source is correct, but a mutation that breaks it
left the suite green. Each addition below was verified by mutating the
source, confirming the existing suite stayed green, then confirming the
new test goes red.

geminiChat.test.ts
- The recovery-coalescing keep test asserted the signed episode and the
  functionCall were both present but never that the episode came first.
  Swapping appendRecoveryContinuationParts's concat to
  [...nextParts, ...mergedParts] left both coalescing tests green.
- The coalescing drop gate had no negative control: nothing covered a
  continuation with NO functionCall, the branch where the unsigned
  trailing episode must be kept. Hardcoding that gate argument to `true`
  popped the episode while all 34 tests in the describe stayed green.

converter.test.ts
- No test fed ensureLeadingAssistantThinking a tool_use turn with no
  thinking block, so the `runStart === -1` guard was unpinned. Removing
  it TypeErrors on blocks[-1] while all 111 tests passed.
- No test placed the first thinking run at the end of the content array,
  so the `runEnd < blocks.length` bound was unpinned. Removing it
  TypeErrors on blocks[blocks.length]. This shape is one this PR itself
  makes reachable, now that episodes persist in stream order.

anthropicContentGenerator.test.ts
- buildThinkingConfig reaches {type:'enabled'} two ways, but only the
  explicit budget_tokens escape hatch was covered. Deriving the gate from
  that input instead of the built config silently disables normalization
  for effort-ladder models while all 229 tests pass.
- The adaptive-mode fixture ended on an unanswered functionCall, so the
  converted request carried a tool_use with no tool_result after it --
  the HTTP 400 shape mergeConsecutiveAssistantMessages's own doc quotes.
  Appending the functionResponse makes it API-valid; the assertion is
  unchanged.

571 tests pass across the three suites. No source file is modified.

* fix(core): gate the dangling-episode drop on genuine trailing-ness, and add a fourth call site

Round-6 review found two Criticals in dropDanglingUnsignedTrailingThought's
call sites, both probe-verified:

- The XML-recovery call site (third site) popped unsigned thought episodes
  that were only ARTIFICIALLY trailing: its own removal loop splices out
  every non-thought text part first, which can manufacture a trailing
  position for an episode that was never trailing in the actual stream. A
  complete, untruncated turn from a non-signing provider that emitted XML
  tool-calls (finish reason present, no truncation) lost its legitimate
  reasoning from history, the JSONL record, and --resume replay for no
  protective benefit. Fixed by capturing trailing-ness BEFORE the removal
  loop and gating the pop on it.

- The transport-continuation prefix insertion had no drop call at all: when
  only thought parts remained, the prefix landed after them, burying a
  trailing dangling episode mid-array before the coalescing-site
  trailing-only check could ever run -- reintroducing the exact wedge the
  other three call sites exist to prevent. Fixed by adding a fourth call
  site immediately before that insertion, accepting the same documented
  false-positive trade-off the other sites already accept.

Updated the two existing tests that pinned the old (buggy) behavior at
each site to assert the corrected behavior instead, and updated the
function's doc comment to describe all four call sites.

* test(core): cover quiet signed reasoning persistence

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

* fix(core): preserve complete Responses reasoning payloads

---------

Co-authored-by: Palanisamy, Dinesh <Dinesh.Palanisamy@netapp.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Dragon <52599892+DragonnZhang@users.noreply.github.com>
2026-09-10 01:48:56 +00:00
Shaojin Wen
a0f0d38d4c
fix(web-shell): avoid duplicate cold session restoration (#11413)
* fix(web-shell): avoid duplicate cold session restoration

* fix(web-shell): expose known capabilities on initial connection

* fix(web-shell): recover from initial workspace discovery failures

* fix(web-shell): preserve discovery errors and stabilize history smoke

* fix(web-shell): retain localized workspace error guidance

---------

Co-authored-by: probe <probe@local>
2026-09-09 22:43:19 +00:00
Dinesh Palanisamy
85ae860cce
feat(core): add OpenAI Responses API content generator (#8169)
* feat(core): add OpenAI Responses API content generator

Adds an HTTP/SSE-only content generator for OpenAI's Responses API
(/v1/responses), selected via AuthType.USE_OPENAI_RESPONSES. Reasoning
continuity round-trips through part.thoughtSignature, matching the
existing Anthropic thinking-signature mechanism, so compaction and
history consolidation work unchanged.

Closes #889

* fix(cli): add openai-responses to AUTH_PREFLIGHT_ENV_KEYS

AUTH_PREFLIGHT_ENV_KEYS didn't have an entry for the new
AuthType.USE_OPENAI_RESPONSES ('openai-responses'), and it isn't in
AUTH_PREFLIGHT_WAIVED_AUTH_TYPES either, so the drift-detection test
(AUTH_PREFLIGHT_AUDITED_AUTH_TYPES covers every AuthType) failed with
expect(uncovered).toEqual([]).

OPENAI_API_KEY is the correct env var, matching the existing mapping
for this auth type in packages/cli/src/config/auth.ts:23
([AuthType.USE_OPENAI_RESPONSES]: 'OPENAI_API_KEY') and
packages/core/src/models/constants.ts's AUTH_ENV_MAPPINGS.

Verification: authPreflight.test.ts 3/3 passing (was failing before
this fix), tsc --noEmit clean, eslint clean.

* fix(openai-responses): address review findings on the streaming pipeline and converter

Addresses the automated /review findings (doudouOUC, qwen-code-ci-bot) left
on #8169 after the AUTH_PREFLIGHT_ENV_KEYS fix.

Critical fixes in responses-pipeline.ts's streamRequest:
- baseUrl fallback used `??`, which does not catch an empty string --
  USE_OPENAI_RESPONSES has no case in ModelRegistry.getDefaultBaseUrl, so an
  unconfigured baseUrl resolves to '' and every request hit the relative
  path /v1/responses. Switched to `||`.
- streamRequest called the global `fetch` instead of the `fetch`
  buildRuntimeFetchOptions pins alongside its dispatcher. Node's built-in
  undici can be a different major version than the bundled one, and handing
  it a foreign dispatcher throws `invalid onError method` -- this broke
  every streaming request whenever a dispatcher was installed (which is the
  common case, proxy or not).
- Network errors from fetch() and non-ok HTTP responses were thrown
  unredacted, leaking proxy credentials into error messages/logs.
- No Content-Type validation: an HTTP 200 non-SSE body (a gateway/proxy
  block page, or a baseUrl pointed at a non-API endpoint) silently yielded
  zero events instead of a surfaced, retryable failure.
- The SSE parser only flushed a pending event on a blank line and never
  after the read loop ended, so a stream that closes without a trailing
  blank line (a proxy stripping trailing whitespace, or an interrupted
  connection) silently dropped its last event -- including the terminal
  response.completed event carrying usageMetadata/finishReason.

Critical fixes in index.ts's embedContent:
- Missing redactProxyError on the embedding error path (present on the
  sibling openaiContentGenerator and on anthropicContentGenerator).
- The embed client passed baseUrl to the OpenAI SDK unnormalized; this
  generator's own /v1-less baseUrl convention (the streaming pipeline
  strips/re-appends /v1 itself) 404s every embedding call against the SDK,
  which does not auto-append /v1 to a custom baseURL. Normalized the same
  way the streaming pipeline does.

Suggestions fixed in responses-converter.ts:
- `buf?.args ?? fc.arguments`: initFunctionCall seeds buf.args to '', so a
  buffer that received output_item.added but no
  function_call_arguments.delta events never fell through to the done
  item's own complete arguments. Switched to `||`.
- A user turn with text + image parts was split into two separate
  'message' items; the Responses API treats them as two distinct turns, so
  the model could fail to associate a question with its image. Parts now
  accumulate per-turn and flush as one message with a multi-part content
  array, in original relative order around any function_call/
  function_call_output/reasoning items.
- Non-image inlineData and all fileData references were silently dropped
  with no diagnostic. They now become a text placeholder noting the
  unsupported attachment (no input_file part type is wired up for this
  generator yet) instead of vanishing.
- Tool results were sent as the raw { output } / { error } JSON envelope
  instead of the unwrapped string the sibling Chat Completions converter
  sends, double-encoding and quote-escaping any tool output that is itself
  JSON.
- Removed ResponsesStreamState.reset(), an unused method contradicting the
  documented one-instance-per-stream contract.

Other:
- Extracted the embedContent text-extraction logic (byte-identical between
  this generator and the sibling openaiContentGenerator) into a shared
  extract-text-from-contents.ts helper.
- Added test coverage: multi-part message merging, attachment placeholders,
  tool-result envelope unwrapping, the buf.args empty-buffer fallback,
  sanitizePromptCacheKey's truncation/hash path, the data-only SSE framing
  branch (the shape the Responses API actually emits, previously untested),
  and the USE_OPENAI_RESPONSES factory route in createContentGenerator.
- responses-pipeline.test.ts previously stubbed the global fetch, which the
  dispatcher/fetch fix above now bypasses (the pipeline calls the pinned
  fetch instead); switched to mocking buildRuntimeFetchOptions directly so
  the mock intercepts the fetch the pipeline actually uses.

* fix(openai-responses): close remaining review gaps from PR #8169

Re-audited every open review thread on PR #8169 against the current
code (commit 09ff5d996). Most had already been fixed in that commit;
this addresses the six that were still open, all Suggestions (no
Criticals remained).

Two real behavior fixes:
- responses-pipeline.ts: a stream that closes with a final `data: `
  line carrying no trailing newline at all was silently dropped.
  `buffer.split('\n')` never routes an unterminated line through the
  main per-line loop, and the existing post-loop flush only handled a
  missing trailing *blank line* (an already-parsed event awaiting its
  terminator), not an entirely unterminated line still sitting in
  `buffer`. Added a targeted post-loop check for this case, with a
  regression test confirmed failing before the fix (0 chunks yielded)
  and passing after.
- index.ts: `getOpenAIClient()` never passed `customHeaders` to the
  OpenAI SDK client, while the streaming pipeline
  (responses-pipeline.ts) already applies them to every request --
  headers configured for the streaming path (e.g. a proxy auth header)
  silently never reached `embedContent`. Added `defaultHeaders`,
  mirroring the pipeline. Regression test confirmed failing before the
  fix and passing after.

Four test-coverage gaps closed:
- contentGenerator.test.ts: the USE_OPENAI_RESPONSES routing test
  only asserted `totalTokens > 0`, which a mocked Chat generator
  would also satisfy. Added a dedicated mock for
  createOpenAIResponsesContentGenerator with a distinguishable
  sentinel and asserted the Chat generator's mock was never invoked.
- index.test.ts: `embedContent`'s `model.includes('embed') ? model :
  'text-embedding-ada-002'` branch was never asserted on either side;
  added the missing `model` assertion to an existing case plus a new
  case for the `includes('embed')` branch.
- index.test.ts: `generateContent`/`generateContentStream`'s
  `request.config?.abortSignal ?? undefined` forwarding was only ever
  exercised with `undefined`. Added cases passing a real
  `AbortController` signal through both methods.
- responses-converter.test.ts: `normalizeResponsesParameters`'s wiring
  into `convertGeminiToolsToResponsesTools` was untested end-to-end --
  every existing tool-conversion case already had `properties`, making
  normalization a no-op. Added a zero-arg-tool case that actually
  exercises it.

Verified: 100 tests passing across the four touched suites (39 + 21 +
18 + 22), each new/changed assertion confirmed to fail on the
pre-fix code where applicable. Typecheck and lint clean.

* fix(core): address review round on the Responses wire (7 criticals + coverage)

Addresses the review round on #8169. Every finding was re-verified against
the code before acting; all were confirmed. Each behavior fix has a
regression test that was checked red-before / green-after in place.

Retry, timeout and error classification:
- generateContentStream returned a lazy async generator, so its promise
  resolved before any network I/O and connection-time 5xx/429 could never
  reach retryWithBackoff. Split the pipeline into an eager connect phase
  (fetch + non-2xx handling + content-type guard) and a lazy body-iteration
  phase behind connectStream(), and wired index.ts to it. executeStream is
  retained and now delegates, so existing callers are unaffected.
- The stream read loop had no timeout layer at all and the undici dispatcher
  uses headersTimeout:0/bodyTimeout:0, so a 200-then-silent upstream hung
  the turn forever with no user present to cancel. Added an idle watchdog
  honoring streamIdleTimeoutMs / QWEN_STREAM_IDLE_TIMEOUT_MS, reusing the
  Chat pipeline's shared constants and its retryable ETIMEDOUT semantics.
  (Note: the reviewer's cited helper `withStreamInactivityTimeout` does not
  exist; the real sibling is `withStreamGuards`, whose structure is mirrored
  here rather than imported, since importing it pulls in the whole OpenAI
  SDK graph.)
- Mid-stream `error` / `response.failed` frames arrive after a 200 OK and
  were thrown as plain Errors with no `.status` and no `.code`, so they
  classified as `unknown` and missed the rate-limit retry, transport replay,
  model fallback and persistent-mode wait. Map the known codes onto a status
  before throwing.
- getOpenAIClient passed raw timeout/maxRetries to the SDK, bypassing
  resolveRequestTimeout (so `timeout: 0`, a documented "disable" knob, meant
  immediate abort) and DEFAULT_MAX_RETRIES.

Tool-call argument handling:
- A corrupt non-empty delta buffer shadowed the done item's authoritative
  `arguments`, dispatching tool calls with `{}`. Fall back to the done
  item's string on parse failure.
- Malformed-but-repairable arguments bypassed the safeJsonParse/jsonrepair
  fallback every sibling wire applies to the same input class.
- Arguments parsing to an array/null/primitive flowed through the
  `as Record` cast into functionCall.args; collapse non-objects to `{}` as
  streamingToolCallParser does.

Fidelity and observability:
- max_output_tokens: buildRequest never read request.config.maxOutputTokens,
  dropping the per-send window clamp geminiChat computes (#5950).
- functionResponse.parts media was silently discarded; tool-returned images
  now reach the wire as a follow-up input_image message.
- The converter never called setGenAiUsageProvenance while always
  materializing cachedContentTokenCount, so every turn reported cache
  statistics as provider-reported.

Reachability and diagnostics:
- AuthType.USE_OPENAI_RESPONSES was missing from the --auth-type yargs
  choices, so `qwen --auth-type=openai-responses` was rejected outright.
- The new PROTOCOL_ITEMS entry was unreachable UI. Added the auth type to
  customProvider.protocolOptions; verified the type is wired end to end
  (contentGenerator creation, AUTH_PREFLIGHT_ENV_KEYS, validateAuthMethod,
  DEFAULT_BASE_URLS) so the wizard option resolves to a working generator.
- systemInfo.ts and the RUM base_url gate were not extended for the new auth
  type, degrading /bug and /about output and losing endpoint attribution.
- De-duplicated the part->text lambda in extract-text-from-contents.

Coverage: filled the 29 flagged untested-behavior sites (multi-line data:
frames, UTF-8 split across chunk boundaries, extra_body precedence with an
explicit 0, custom header forwarding, abort-signal identity and mid-stream
abort, dispatcher forwarding, proxy-credential redaction, the non-SSE
content-type guard, malformed frames, empty-merge, and the auth/telemetry
entries).

Verification: 111/111 in openaiResponsesContentGenerator, 54/54 in the other
touched core suites, 402/404 in the touched CLI suites (the 2 failures are
pre-existing and reproduce on a pristine checkout -- config.test.ts reads
the ambient OTEL_EXPORTER_OTLP_ENDPOINT). tsc clean, eslint clean, prettier
clean.

* refactor(core): actually de-duplicate the part->text lambda in extractTextFromContents

Correction to dcd8a67d9, whose commit message claimed this was done. It was
not -- the subagent that owned the file returned a WONTFIX for it and the
claim went into the message unverified. The lambda was still duplicated
verbatim between the array-input and single-content branches of the very
helper whose JSDoc promises "a change to how Part text is shaped only needs
to be made once".

Extracted `textOfParts` and routed both branches through it. Behavior is
unchanged for every input shape.

Added the collocated test file the helper never had, including a guard that
converts the same Content bare and wrapped in a single-element array and
asserts the two agree -- the drift this extraction exists to prevent, and
which the pre-extraction code had already let happen once. Verified by
re-injecting a divergent copy into the array branch: that test alone goes
red, the other five stay green.

117/117 across the Responses directory plus the new file; tsc, eslint and
prettier clean.

* fix(core): address round-2 review on the Responses wire

The first automatic review to complete on this PR (previous three attempts
timed out). 30 inline findings plus five Criticals that were recorded in the
review ledger but never posted inline. Every finding was verified against
the code before acting.

Pipeline:
- max_output_tokens precedence reopened the user's ceiling. Last round used
  `request.config?.maxOutputTokens ?? samplingParams.max_tokens`, which
  returns the larger per-send value for config 1000 + per-send 8000. Now
  routes through tokenLimits' `reconcileMaxTokens` "smaller wins" invariant,
  which both sibling wires call and which exists precisely so a new provider
  can't silently reopen that ceiling. The reviewer was right about our own
  previous fix.
- `tool_choice: 'auto'` and `parallel_tool_calls: true` were sent
  unconditionally. Toolless requests are reachable here (compaction via
  baseLlmClient.generateText, text-mode side queries) and strict endpoints
  400 on tool_choice with no tools, which can wedge compaction in a retry
  loop. Gated on tools being present, matching the sibling.
- The connect phase had no timeout at all, and `ContentGeneratorConfig.timeout`
  was never read by this pipeline. An endpoint that completes TCP/TLS but
  never sends headers blocked the turn forever: the pinned undici Agent runs
  headersTimeout:0/bodyTimeout:0, the idle watchdog only wraps reader.read()
  inside iterateBody, and retryWithBackoff has no per-attempt timeout.
  Added StreamConnectTimeoutError via the same resolveRequestTimeout helper
  the embed client uses.
- Only the idle watchdog existed, so an upstream drip-feeding one chunk per
  window re-armed it forever. Added StreamLifetimeExceededError using the
  sibling's shared DEFAULT_STREAM_MAX_LIFETIME_MS /
  QWEN_STREAM_MAX_LIFETIME_MS_ENV constants, and folded the idle/lifetime
  resolvers into one helper that now warns on invalid values instead of
  silently discarding them.

Converter:
- A delta buffer that parsed cleanly to a non-object (array/null/primitive)
  bypassed the done-item authoritative-arguments fallback, which only ran in
  the catch branch, so a tool call dispatched with `{}` while valid
  `arguments` sat in the same event. Residual of last round's fix, which
  covered only the parse-throwing case.
- Refusals were silently dropped: no `response.refusal.delta` / `.done`
  handling, so a refusal produced zero content chunks, threw
  NO_RESPONSE_TEXT, and retried four times re-sending the full prompt before
  surfacing a misleading error. Added both events and the refusal content
  part; deltas emit as text, mirroring output_text.delta.
- Tool-parameter precedence was inverted relative to both siblings
  (`parameters ?? parametersJsonSchema`); swapped to match.

Generator, CLI and SDKs:
- LazyContentGenerator.useSummarizedThinking() omitted the new auth type.
- The custom-provider test mixed barrel imports (resolving to dist/) with
  source imports, so the per-file vitest flow saw an undefined
  AuthType.USE_OPENAI_RESPONSES against a stale dist. Both sides now come
  from source.
- Both bundled SDKs enumerate every CLI auth type but omitted this one, so
  SDK users could not select it: added to the TypeScript zod enum and union
  and to the Python Literal and validation set, with a parametrized test
  pinning the whole set against the CLI's --auth-type choices.

Coverage: filled the flagged gaps across all three files — per-output_index
function-call isolation, cached_tokens:0 absent-vs-zero, the reasoning
shape-guard, functionResponse text-append, string systemInstruction,
multi-Tool conversion, non-mutation recursion, proxy hand-off, apiKeyEnvKey
fallback, Content-Type, error-body excerpt, .status stamping, ETIMEDOUT
code, per-chunk timer reset, the no-body guard, empty-string baseUrl, the
countTokens fallback arithmetic, maxRetries pass-through, the no-baseUrl
embed branch, trailing-slash normalization, and abort-signal null handling.

Every behavior change is mutation-verified: fix reverted, test observed red,
fix restored.

Not changed, flagged for maintainers:
- `prompt_cache_key` is keyed on `userPromptId` (`${sessionId}####${counter}`),
  which changes every turn and so defeats cross-turn prefix-cache affinity.
  Keying on the stable session prefix is a behavior change, so it is raised
  rather than made unilaterally.
- ACP `buildAuthMethods()` advertises only USE_OPENAI — it does not advertise
  Anthropic, Gemini or Qwen OAuth either, so this is pre-existing scope
  rather than a gap introduced here.

One ledger Critical (the pinned `fetch` being discarded in favour of Node's
global) is INVALID as written: it describes pre-round-1 code. `fetchFn =
runtimeOptions?.fetch ?? fetch` landed in 09ff5d996, and the pipeline tests
deliver their mock through `runtimeOptions.fetch` without stubbing the
global, so a regression to global fetch would fail the suite.

Verification: 174 core tests across the six touched core suites, 419 CLI
tests across seven, 119 Python SDK unit tests; tsc clean for core, cli and
sdk-typescript; eslint, prettier and ruff clean.

* fix(core): key the Responses prefix cache on the session, not the turn

`prompt_cache_key` was set to `sanitizePromptCacheKey(userPromptId)`, and
userPromptId is `${sessionId}########${counter}` -- it changes on every send.
Each turn therefore got its own cache namespace and no request in a session
could ever hit the prefix cache the previous one wrote. Nothing fails when
that happens; the entire cost is silent latency and spend, which is why it
survived until review flagged it.

Keyed on `qwen-code:${sessionId}` instead, matching the Chat wire's
`applyOfficialOpenAIPromptCaching` (openaiContentGenerator/prefix-caching.ts).

Note on provenance, since it changes how this reads: that sibling behavior
landed on main in 6897ef744 (#8418) on 2026-08-07, which is AFTER this branch
last merged main (770146526) and is not in this branch's history. So this was
not a case of ignoring an existing helper -- main converged on session-scoped
keying independently while this PR was in review, and the two wires would
otherwise have disagreed on merge.

The `qwen-code:` prefix is written as a literal here because
prefix-caching.ts on this branch's base predates the constant. Once main is
merged in, this should import the shared CACHE_KEY_PREFIX rather than keep a
second copy of the string.

`Config.getSessionId()` is typed `string` and never returns undefined, so the
guard covers the reachable empty-string case only; the mock and the fallback
test were written against that rather than a `undefined` state the real API
cannot produce.

Test pins the actual property -- two different userPromptIds in one session
produce the same key. Reverting to per-turn keying fails it. 135 tests pass
across the directory; tsc, eslint and prettier clean.

* fix(core): route the setup wizard's enable_thinking through reasoning.effort on the Responses wire

wenshao's local verification report on #8169 found that picking
`openai-responses` in the setup wizard and enabling "Enable thinking"
produced a session with zero reasoning, because the wizard writes the
DashScope-specific `extra_body.enable_thinking` toggle for every
protocol. The Responses wire has no DashScope-detection layer (unlike
the OpenAI Chat pipeline), so it forwarded `enable_thinking` onto the
wire verbatim as an undefined field instead of requesting reasoning at
all.

- provider-config.ts: buildAdvancedGenerationConfig now takes the
  resolved protocol and writes `reasoning: { effort: 'medium' }`
  instead of `extra_body.enable_thinking` when the protocol is
  `openai-responses`.
- responses-pipeline.ts: defense in depth for configs that still carry
  a legacy `extra_body.enable_thinking` (hand-edited settings, ACP,
  etc.) — buildReasoning() treats it as `effort: 'medium'` when no
  explicit `reasoning` is set, and the extra_body merge always strips
  the key so it can never reach the wire literally.
- docs: add `openai-responses` to the supported-auth-types and SDK
  tables, a full config example, a row in the reasoning per-provider
  table, and correct settings.md's `extra_body` note (it previously
  said the field was `openai`/`qwen-oauth` only).

Addresses PR #8169 review findings 3.1 and 3.2.

* fix(core): reject unsupported Responses image MIME types

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

* fix(responses): recover rejected reasoning replay once

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

* fix(responses): reject malformed replay errors

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

* fix(responses): constrain proxy error recovery

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

* fix(auth): expose OpenAI Responses consistently

* fix(responses): bound error response handling

* fix(responses): honor request-scoped controls

* fix(auth): align Responses setup preview

* fix(responses): preserve request thinking opt-out

* fix(responses): require SSE content type

* test(cli): include Responses auth in top-level help expectation

* fix(responses): address protocol compatibility review

* fix(responses): redact diagnostics and normalize custom headers

---------

Co-authored-by: Palanisamy, Dinesh <Dinesh.Palanisamy@netapp.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Dragon <52599892+DragonnZhang@users.noreply.github.com>
Co-authored-by: probe <probe@local>
2026-09-09 22:36:23 +00:00
顾盼
fbb877a48e
fix(cua): preserve degraded UIAccess responses (#11479) 2026-09-09 13:25:35 +00:00
Shaojin Wen
50b942ae0d
feat(web-shell): improve session overview navigation and details (#11238)
* feat(web-shell): improve session overview navigation and details

* fix(web-shell): keep session details inside constrained viewports

* test(web-shell): Verify settled session details replacement

* fix(web-shell): Preserve overview state and keyboard focus

* fix(web-shell): Fix rename-time control clicks and details focus ownership

- Narrow the inline-rename mousedown exemption to the editor input, so
  the blur-cancel remount can no longer detach a pressed control before
  mouseup and swallow its click; the draft now survives in-row control
  activation while plain-cell clicks still preserve it.
- Scope the details-popover focus rescue to a shared owner token: both
  overview entry points replace each other as before, but a hover from
  another owner (e.g. a sidebar row) no longer moves focus out of — and
  silently dismisses — a pinned overview popover.
- Pin the witnesses: cue/column icon agreement, no cue on idle rows,
  portal-root ancestry of the overview dialog, and a checkbox click
  landing while a rename draft is open.

* test: Fix CI details assertion and supervisor teardown

---------

Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-09-09 12:10:00 +00:00
ytahdn
2c5250daa6
feat(web-shell): keep question answers visible as user-style messages (#11461)
Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
2026-09-09 12:05:57 +00:00
qqqys
774101619a
feat(sessions): record what kind of session registered, and document the registry and wire contract (#11463)
* feat(sessions): record what kind of session registered, and document the registry and wire contract

A registry record said what a session was called and where it was
working, but not what it was. That was fine while the interactive UI was
the only thing that registered; it stops being fine the moment anything
else does. A listing cannot tell a terminal someone is sitting at from a
session a daemon drives, and an aggregator building a "here is everything
running" view has to guess.

Records now carry `kind`: `tui`, `headless`, `serve`, or `external`. The
interactive UI writes `tui`; the other three are for registrants that do
not exist yet, and for programs that are not Qwen Code at all. It is a
label, not a credential — a record is written by the process it
describes, so this is a claim like `name` and `cwd`, and nothing that
decides what a sender may do reads it. `qwen sessions ps` grows a KIND
column, `--json` carries the field, and `list_agents` passes it to the
model with a sentence saying exactly that much and no more.

A reader keeps a kind it does not recognize rather than correcting it: a
newer build writing `relay-2` is describing itself accurately, and
replacing that with "unknown" would lose the only thing the record
actually says. An absent one reads as `tui`, which is not a guess —
nothing else registered before the field existed. Malformed or over-long
values are dropped, because absent has a meaning of its own and a
`kind` is bounded input like every other field that crosses a process
boundary.

`registerSession` also takes an explicit name now. A program whose
working directory says nothing useful about it — a voice front-end, a
relay — should be called what its user calls it rather than after
whatever directory it started in. Flattened and bounded on the way in,
falling back to the derived name when it flattens to nothing, because an
unaddressable session is worse than a generically named one.

The larger half of this is a page rather than code. Everything an
external program needs was already true and nowhere written down: the
record schema and how liveness is judged, the socket paths and framing,
the auth line and what each of the three tokens establishes, every frame
field and its validation, the receipt states and the transitions between
them, and what a receiver does with a message before its model sees it.
Writing it down is what lets a program take part without reading our
source — and the page is explicit that the auth line is the whole trust
model, so nobody reads `kind: "external"` as something to lean on.

Part of #10925
Unblocks #10118 M3

* test(cli): assert the kind the interactive UI registers with

The suite pinned the whole argument object `startInteractiveUI` hands
`registerSession`, so adding a field to that call had to be stated here
too. Stated rather than loosened: this is the one registrant a listing
describes as someone sitting at a terminal, and this call is the only
place that claim is made.
2026-09-09 11:33:43 +00:00
callmeYe
40c9506d60
feat(core): expand Kimi, Qwen and DeepSeek reasoning presets (#11349)
* feat(core): expand Kimi Qwen and DeepSeek reasoning presets

* docs(core): clarify Qwen reasoning compatibility aliases

* fix(core): remove redundant DeepSeek thinking default

* docs: align reasoning design with bilingual requirements
2026-09-09 10:54:37 +00:00