mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-09-12 03:57:07 +00:00
1775 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f58809c730
|
Merge branch 'main' into codex/model-api-selection | ||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
18abbc8cb4
|
Merge branch 'main' into codex/model-api-selection | ||
|
|
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. |
||
|
|
181fd2ae7e
|
Merge branch 'main' into codex/model-api-selection | ||
|
|
91a09e76ae
|
fix(core): preserve Responses retry metadata and assistant phases (#11652) | ||
|
|
9969640815
|
feat(web-shell): Edit and resend the latest user message (#11650)
Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com> |
||
|
|
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
|
||
|
|
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> |
||
|
|
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. |
||
|
|
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 |
||
|
|
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. |
||
|
|
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 |
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
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.
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
57e828a215
|
refactor(dingtalk): remove obsolete background response aggregation (#11570)
Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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 (& 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> |
||
|
|
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 |
||
|
|
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>
|
||
|
|
d030e51b49 | chore: merge origin/main to include CI fixes | ||
|
|
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> |
||
|
|
eabad6ed95 | chore: merge origin/main into codex/model-api-selection | ||
|
|
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> |
||
|
|
1af6094b77 | feat: select the OpenAI API per model | ||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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
|
||
|
|
fbb877a48e
|
fix(cua): preserve degraded UIAccess responses (#11479) | ||
|
|
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> |
||
|
|
2c5250daa6
|
feat(web-shell): keep question answers visible as user-style messages (#11461)
Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com> |
||
|
|
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. |
||
|
|
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 |