mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
b2dfca6fb42c2be17cbb97d86ce82024ac9cd463
9169
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b2dfca6fb4 |
feat(cli): OpenTUI migration live-session and input batch (#10368)
* feat(cli): OpenTUI foundation modules — theme, a11y, clipboard, keys, dialogs scaffolding
Foundation batch of the OpenTUI migration tracked in #8662. Adds the renderer-neutral foundation modules under ui/opentui: theme family, a11y (plain-text, screen-reader), clipboard, key-map, mouse hit/caret, link-click + osc8 parity, early-input, exit guard/lifecycle, kitty negotiation, event-adapter, item-projection, slash dispatch (+ command parsing), commands context/output, help content, input history, and the dialog scaffolding primitives (core/shared) with the theme dialog. Two helpers land inside ui/opentui rather than utils/ to respect the utils leaf-layer rule (#9737). Stacked on the infra batch: consumes ui/model streaming model and @opentui deps. No reachable ink code changes beyond a one-line export addition in the shared osc8 module.
* fix(cli): import originals instead of forking slash parser and dialog scope utils
* fix(cli): address R1 review findings in OpenTUI foundation modules
* fix(cli): align OpenTUI command host with the memory-file-count rename
Upstream renamed setGeminiMdFileCount to setMemoryFileCount in the command
UI contract; the rebase onto main surfaced the mismatch at build. Rename
the host interface member, the bridge wiring, the dispatch stub, and the
test mock to match.
* feat(cli): OpenTUI migration live-session and input batch
Third landing batch of the OpenTUI migration (#8662): live-session stream
fold and model, message rendering (markdown heal, MCP progressive, client
tool runs, text batching), transcript adapter with resume/session-switch,
sticky todos, the composer (input-prompt view/key/model), mouse rows and
scrollbar, unified-diff rendering, and session-compaction notice. All
additive — no reachable ink code path is touched, ink remains the default.
Carries the first consumer of the remend dependency deferred from the
infra batch, placed in devDependencies per the renderer-deps convention.
The stacked-skill completion helpers import from the relocated
ui/commands module following the upstream rename.
* fix(cli): address R2 review findings in OpenTUI foundation modules
Round-2 review fixes (17 Critical + 10 Suggestion resolved in code):
- dialogs-shared: move number-select flush out of the setState updater
(StrictMode double-fires onSelect); split setActiveIndex (ink
SET_ACTIVE_INDEX, lands on any in-range row) from highlightIndex
(arrow keys skip disabled rows) so wheel navigation never sticks
- event-adapter: chat_compressed notice mirrors ink formatCount ('~'
prefix for estimated counts); vision_bridge_notice renders
summary\nnotice; explicit projections for task_execution /
findings_list / terminal_image keep multi-MB payloads off the
transcript; retry-countdown-clear forwards isContinuation
- slash-dispatch: isSlashCommandInput drops the '?' branch (ink gate
routes ? input to the model); executeSlashCommand races the action
against the abort signal; dialog effects carry the
OpenDialogActionReturn payload; projected added-item text surfaces
alongside non-handled effects (notice); message-shaped items project
to their text; ui.history comes from env; absent sessionStats stamp
now, not epoch; telemetry parity (recordSkillInvocation /
recordAutoSkillCommandUsage / makeSlashCommandEvent)
- item-projection: model stats render per-(model,source) sections with
N/A for unpriced entries; Tool Calls line uses ASCII x like ink;
redactProxy deduplicated via systemInfoFields export
- theme: palette/syntax colors resolve through color-utils toHex before
parseColor (ink CSS names / *bright names no longer degrade to
magenta); unresolvable values stay unset
- key-map: kitty 'kpenter' normalizes to 'return'; resolveCommands
exposes ink's key fan-out (Ctrl+C fires QUIT + CLEAR_INPUT)
- a11y: hardWrap delegates to wrap-ansi (word-boundary parity with
ink's screen-reader path); markdown reducer tracks fence length,
keeps fence-like lines literal inside fences and inner backticks in
multi-backtick spans; stripAnsi delegates to strip-ansi plus a
private-parameter CSI pass (SGR mouse, DEC save/restore)
- clipboard: OSC 52 write gated on a TTY (stderr preferred), tests spy
the stream instead of writing real sequences to the runner's terminal
- exit-guard: independent per-key arm windows like ink
- dialogs-theme: diff preview pane receives syntaxStyle/filetype
* fix(cli): harden kitty probe and screen-reader writer per maintainer review
- kitty-negotiation: KITTY_REPLY_RE requires at least one flag digit
(\d+), so an echoed bare query \x1b[?u in PTY/CI environments no
longer resolves true and locks the renderer into kitty mode on a
terminal that never answers queries; the accumulation buffer keeps
only a 256-byte tail (bounded memory, bounded rescan under byte
floods); the settle-window drain is removed — an EventEmitter data
listener cannot consume chunks from other listeners, so late replies
flow to the renderer's input parser like any other terminal noise
- a11y-screen-reader: ScreenReaderOutputWriter sanitizes written
content (stripAnsi + drop bare C0/C1 controls, keep newlines) so the
plain-text-only contract is enforced at the writer instead of
trusting every future caller — smuggled OSC 52 clipboard writes or
title/cursor sequences cannot execute on the main screen
* fix(cli): address ytahdn independent review findings in OpenTUI foundation
All 15 findings from the independent static review verified in source and
fixed (no false positives; none deferred):
- quit effect carries QuitActionReturn.messages projected to text on a
notice field — ink renders them via QuittingDisplay and the payload was
permanently lost (Important #1)
- error and finished branches emit retry-countdown-clear like ink's
handleErrorEvent/handleFinishedEvent, so a terminal event inside the
countdown window no longer leaves a stale retry row (#2)
- projectContextUsage renders the compaction-threshold ladder and the
per-item detail sections (tools/memory/skills, ink's sort order) when
showDetails is on — /context detail transcripts no longer show strictly
less than the compact view (#3)
- projectMcpStatus honors showSchema (parameter JSON under each tool) and
showTips, so /mcp schema is distinguishable from /mcp (#4)
- SlashDispatchEnv.settings is required: the real
CommandContext.services.settings is non-null and a null surfaced as a
generic command failure on first .merged read (#5)
- dead singleColumn flag removed from the help width layout (clamp makes
it always false; ink has no single-column mode) (#6)
- truncated SS3 tail (bare ESC O) is stripped like the truncated CSI
tail, so a captured half F1-F4 no longer leaks 'O' into the composer (#7)
- readBufferRow trims cellColumns alongside text, so URLs ending at
end-of-row on a wide character hit-test on both halves of the cell (#8)
- kitty probe writes guarded: a synchronous stream throw settles the
probe (restores raw mode, removes the listener) instead of leaking (#9)
- eraseLines reuses the ansi-escapes helper (already a repo dependency)
instead of a byte-identical hand-rolled copy (#10)
- truncateText passthrough wrapper dropped; callers use the exported
truncateHelpText directly (#11)
- SkillsList truncate keeps total length n like ink, so the description
column no longer shifts by one cell when a name truncates (#12)
- model_fallback names pass through sanitizeDisplayText like ink (#13)
- selectIndex fires onHighlight before onSelect (ink dispatches
SET_ACTIVE_INDEX then SELECT_CURRENT), keeping highlight-driven
stay-open dialogs synced on mouse input (#14)
- mcp_app without fallbackText renders empty instead of JSON-dumping the
embedded HTML; projectAbout hides Base URL when selectedAuthType is
empty, matching ink's formatBaseUrl (#15)
* fix(cli): address R3 review findings in OpenTUI foundation modules
- executeSlashCommand catch checks the abort signal first: an ESC-
cancelled command (action rejects AbortError) returns handled with no
failure telemetry or error message, mirroring ink's processor — the
race promise never resolves when the signal is already aborted at
addEventListener time
- submit effect carries the full SubmitPromptActionReturn contract
(modelOverride, onComplete, refreshContextFilesOnWrite) so the
backend can honor /model <id> <prompt>, /dream's manual-run record,
and /remember's context refresh like ink instead of silently
degrading them
- closing fences cannot carry info text (CommonMark): a ```js line
inside an open block is literal body, not an early close that drops
the block and inverts parse state for the rest of the document
- info items append their linkUrl/linkText footer (ink's InfoMessage
renders it; headless/SSH users need the printed URL, e.g. /bug)
- the screen-reader sanitize keeps TAB: it separates words in
tool/model output and deleting it fused adjacent tokens
* fix(cli): address R4 review findings in OpenTUI foundation modules
- a11y-plain-text: split on all CommonMark line endings so CRLF
markdown opens/closes fences correctly; private-param CSI regex
covers ECMA-48 intermediate bytes; DCS/SOS/PM/APC and unterminated
OSC sequences consumed before strip-ansi; code-span pattern mirrors
ink's INLINE_CODE_SPAN_PATTERN (non-empty content, closing-run
lookbehind)
- dialogs-shared: clearNumberBuffer called from setActiveIndex,
selectIndex, and resyncKey block so wheel/hover/click/resync can't
commit a stale numeric-flush selection the user never made
- event-adapter: tool_call_response carries visionBridgeNotice on the
tool-result event (ink ToolMessage renders the egress disclosure)
- item-projection: projectContextUsage reads memoryFiles as { path,
tokens } (ContextMemoryDetail), not { name, tokens }
- key-map: 10 kp* keypad-navigation aliases (kpleft→left, …) and
super flag folded into meta (ink Cmd+Enter = newline, not submit)
- link-click: cellColumns no longer truncated to trimmed text length
(preserves the wide-glyph right-half boundary); findUrlAtRow end
boundary is width-aware (stringWidth of the last glyph)
- slash-dispatch: submit effect carries PartListUnion content + a
textContent string for text-only consumers (image parts survive);
toggleVimEnabled and startNewSession seams wired from env; abort
race resolves immediately for an already-aborted signal
- clipboard: OSC 52 self-write removed — copyToClipboard's existing
fallback (writeOsc52 / wrapForMultiplexer) is the single source
- a11y-screen-reader: appendStatic skips clean === '\n' (ink's
hasStaticOutput guard)
* fix(cli): address #10383 R1 findings in foundation modules
- event-adapter: finished branch emits retry-countdown-clear BEFORE the
info notice so the countdown row is actually cleared (the fold only
pops when the last item is the retry row)
- item-projection: /mcp tips now include all 5 lines ink renders (added
OAuth auth tip and Ctrl+T toggle tip)
- link-click: wide-glyph end boundary uses the last code point (not
UTF-16 code unit) so non-BMP emoji are measured correctly by
stringWidth
- a11y-plain-text: CSI_SEQUENCE replaces PRIVATE_PARAM_CSI — drops the
marker requirement so any CSI (with or without private parameter
marker, with or without intermediate bytes) is fully consumed
* fix(cli): address #10383 R2 Critical findings in slash-dispatch
- abort race: already-aborted signal now skips command.action entirely
(result = undefined) instead of eagerly evaluating it as a Promise.race
argument — the action's side effects (clear, persist, addItem) must not
run on a cancelled submission
- parent command telemetry: logEvent (slash_command SUCCESS) is now
called before the early return for parent commands with subCommands
(help listing) and bare handled — matching ink's finally-block logging
* fix(cli): address #10368 R2 review findings in live-session batch
- input-prompt: convert OpenTUI display-width cursor coordinates to
code-point positions at the component boundary — the pinned
@opentui/core reports logicalCursor.col/offset and el.cursorOffset in
terminal-cell units (edit-buffer.zig), while the ported ink helpers
work in code points; wide characters previously shifted placeholder
backspace, the backslash continuation check, completion targeting,
and history edge compares
- input-prompt: bump both search sequence refs when Esc dismisses the
completion dropdown so an in-flight search resolving afterwards cannot
re-open it and hijack Enter
- live-session-model: carry the vision-bridge egress disclosure through
the tool-result fold (ink ToolMessage renders it under the result)
- messages: recognize the producers' two-L 'cancelled' summary spelling
so canceled tools get the CANCELED glyph with strikethrough instead of
the red ERROR glyph
- session-switch: wrap /resume and /branch in the telemetry swap
transaction (begin before the outgoing-session capture, commit at the
UI re-key, abort after a rolled-back swap) — restores the usage
aggregate on failed swaps and rejects concurrent switches
- tests: display-width fake editor, wide-char placeholder/continuation
witnesses, Esc invalidation, fold notice, cancelled spelling, and the
three swap-transaction lifecycle cases
* fix(cli): declare cursorOffset on the FakeEditor test interface
The display-width fake added in
|
||
|
|
60d8c93e27 |
feat(channels): Improve fallback permission and error messages (#10614)
* feat(channels): show tool details in permission requests * feat(channels): Improve DingTalk error fallbacks * fix(channels): Harden fallback message details * fix(channels): Correct misclassified inbound errors and pin classifier branches --------- Co-authored-by: qqqys <[email protected]> |
||
|
|
4ea865acd8 |
fix(core): keep image views working with sharp releases older than 0.34 (#10633)
`metadata.autoOrient` only exists in sharp >= 0.34. On installs that must stay on older sharp releases (for example because newer prebuilt libvips cannot load on the host libc), `read_file` image views and `zoom_image` crash on every image with "Cannot read properties of undefined (reading 'width')". - Add `orientedSize()`: prefer `metadata.autoOrient` when present, otherwise derive the oriented size from the stored dimensions and the EXIF orientation tag (orientations 5-8 swap the stored axes). - Apply EXIF orientation with `.rotate()` instead of the `autoOrient` constructor option, which does not exist before sharp 0.33. Both forms behave identically across the old and new sharp series, including extract-in-oriented-space semantics. Covered by orientedSize unit tests and EXIF-rotated overview/crop integration tests. |
||
|
|
20fe29be8c |
feat(serve): allow relocating session attachment storage via env var (#10066)
* feat(serve): allow relocating session attachment storage via env var Adds QWEN_SERVE_SESSION_ATTACHMENTS_ROOT, which stores session attachments under <root>/<projectHash>/attachments instead of the runtime temp dir so operators can pin them to a dedicated volume. Reads and removes that miss the configured root fall back to the default dir so pre-switch attachments stay readable and removable; archive cleanup removes both roots. New uploads never shadow a legacy fallback name, and both roots are removed via the same tombstone dance so a deletion racing a session restore cannot sweep up a successor directory. * fix(serve): keep attachment root resolver off fast path * fix(serve): harden session attachment fallback against degraded roots read() and remove() no longer force-create the configured root before consulting the fallback, so a degraded configured volume serves and removes pre-switch attachments from the healthy default dir instead of failing; delete() removes the fallback root first, mirroring remove(), so a failure on the legacy root keeps the primary copy intact; QWEN_SERVE_SESSION_ATTACHMENTS_ROOT is trimmed before use. Corrects the docs to say attachment cleanup happens on session delete, not archive. * fix(serve): address round-2 review findings on session attachment storage (#10066) Co-authored-by: Qwen-Coder <[email protected]> * fix(cli): stop restore-probe tests depending on ambient host git state (#10066) * fix(acp): stabilize attachment fallback handling * fix(acp): skip attachments being removed during copy --------- Co-authored-by: 钉萁 <[email protected]> Co-authored-by: Qwen Code Autofix <[email protected]> Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: qwen-code-dev-bot <[email protected]> Co-authored-by: wenshao <[email protected]> |
||
|
|
409ae7b053 |
feat(web-shell): derive session issue bindings from the closing references of bound PRs (#10425)
* feat(web-shell): derive session issue bindings from the closing references of bound PRs Sessions could be found by the PR they produced, but not by the issue they were working on — the maintainer flow starts from an issue and ends in a PR. GitHub already knows that link: the PR body's "Fixes #N" is its closing reference. Snapshot those references, with the issue state, onto each bound PR entry in the existing sidecar instead of adding a client-side binding, a second sidecar, or a prompt/branch heuristic that would misbind across the shared PR/issue number space. The refresh sweep gains a second, by-number GraphQL lookup: every non-merged binding (closing references change while a PR is open) and every merged binding that predates the snapshot (one catch-up) is queried in batches, and the result is written in place together with the PR state — order and createdAt untouched, url-mismatched (foreign repository) entries never touched, unchanged sidecars never rewritten. The slim PR list query stays as it was: nesting closing references under it measurably slows it and still carries no issue state, while a by-number lookup also reaches PRs outside the 500-entry window. gh exits non-zero over a NOT_FOUND alias yet still prints the other aliases, so the wrapper parses that payload rather than failing the batch. Every projection from the sidecar to the wire now goes through one shared helper, the bridge keeps the daemon-derived snapshot across a client re-bind, and the SDK guard validates issue entries with the same url rules as PRs. The Web Shell tooltip lists the issues under the PR rows (deduped across stacked PRs) with GitHub-style state icons, and sidebar search matches an issue number with or without the hash. * fix(serve): converge foreign merged bindings and gate every snapshot carry on the PR's url Review round 1 on the issue-snapshot sweep. A merged binding the repository cannot resolve — another repository's same-numbered PR — could never receive a snapshot, so it re-entered the by-number lookup on every sweep and broke the "all merged with snapshots costs no call" invariant; the sweep now writes it a converging empty snapshot once the lookup has succeeded, and only ever stores issues the lookup actually resolved for that url. The session-list merge of persisted and live bindings compared numbers alone, the one carry site that did not check the canonical url, so a cross-repository re-bind could briefly wear the previous repository's issues; it now applies the same url gate as the sidecar and bridge. The closing-references fetch bound and the sidecar's per-PR cap were two literals tied by a comment (raising one would void every sidecar on read); they are one constant now, declared in the utils layer the sidecar service imports. The GraphQL wrapper passes its own timeout to the error formatter, and the write-side binding types on the bridge and the SDK omit issues so a client-bound issue list is a compile error, matching the runtime drop. Tests pin each of these plus the gaps the review found: the killed-timeout message, the list-query failure still snapshotting issues, client-supplied issues dropped by the bridge, the cross-repository re-bind dropping the snapshot, a closed issue with no state reason mapping to completed, the open issue-state rendering, the full `-f query=` argument, and the SDK guard's number and non-object checks. * fix(serve): retire unresolvable bindings from the issue sweep and reject non-safe PR numbers Review round 2 on the issue-snapshot sweep. A merged binding no lookup can ever snapshot now converges in every case where that is knowable — the lookup succeeded without resolving it, the lookup is structurally impossible (no gh binary, no git root), or the platform has no closing references at all (Aone) — while a transient failure still retries, since it says nothing about the PR's references. A non-merged binding to another repository stays out of the lookup altogether: the list query names this workspace's repository, and a binding outside that canonical prefix can never resolve here, the GitHub twin of the Aone refreshability filter. A workspace with only unresolvable bindings therefore costs nothing after one sweep, and a mixed workspace still refreshes its open bindings' state. The GraphQL wrapper accepts safe integers only: a positive integer-valued double at or beyond 1e21 stringifies in exponential notation, an invalid Int literal that failed the whole document instead of one alias — one such binding would have disabled issue refresh for the entire workspace. The session-list merge finds the persisted entry by url rather than a last-wins number map, so a hand-edited sidecar with two same-numbered entries cannot shadow the live binding's own snapshot. Tests cover each path (lookup resolving nothing, foreign open binding never queried, gh unavailable converging, transient failure retrying, mixed workspace running the list query, generation closing between the two queries, merged-only Aone workspace going quiet, the repository scope in the query, and the safe-integer bound), and the E2E plan's bridge filter now selects the client-supplied-issues test. * fix(serve): reject partial GraphQL errors and match issue lookups by PR identity Review round 3. The response parser read only the repository data and dropped the top-level GraphQL errors, so a server error nulling one alias, or a sub-field error nulling a resolved PR's closing references, came back as a successful lookup — and the sweep then retired a merged binding on that "absence" with a permanent empty snapshot. Only an alias-level NOT_FOUND now counts as absence; every other partial error fails the call, which keeps retrying. A repository gh cannot resolve at all (no git remotes, or none on a GitHub host) is a structural failure, not a transient one, and converges like a missing gh binary; execFile's error carries no stderr of its own, so the wrapper attaches it before classifying. Lookup results are matched to bindings by host, owner, repository, and number rather than by canonical url, so a binding spelled with `www.`, `http:`, or a `/files` suffix receives the issues fetched for it — written under the binding's own url, which the sidecar's canonical write gate requires — instead of being mistaken for a foreign repository and retired empty. The SDK guard caps issue lists at the sidecar's ten, matching the design. Tests: partial-error rejection with the NOT_FOUND tolerance kept, the repository-unresolved kind, the variant-url binding keeping its fetched issues, convergence under not_a_repo and repo_unresolved, a foreign merged binding converging during a lookup outage, the Aone workspace never reaching the GitHub lookup and never re-resolving its origin once quiet, the canonical-equal url spelling inheriting the sidecar snapshot in the session list, the sanitized env dropping GH_REPO, the exact safe-integer literal, and a state-less issue accepted by the SDK guard. * fix(core): treat a repository GitHub no longer serves as structurally unresolved Review round 4. A renamed, deleted, or access-revoked repository answers the lookup with a NOT_FOUND on the repository itself rather than on an alias; that was thrown as a partial error and reported as a transient failure, so merged bindings in such a workspace never converged and the sweep kept paying the failing lookup. It now classifies as the same structural kind as a missing GitHub remote, which the sweep already retires bindings on. Test strengthening the round asked for: one fixture per accepted gh diagnostic, the repository-level NOT_FOUND payload, the SDK guard accepting exactly ten issues and rejecting a control character in an issue url, and the tooltip fixture reworked so binding order contradicts number order, a filtered PR's issues disappear with it, a same-numbered issue from another repository stays distinct, the merged PR contributes an observable issue, non-openable issues leave no text behind, and the state-less icon check reads the class token list instead of jsdom's SVGAnimatedString. * fix(serve): keep GH_HOST mismatches transient and match every GitHub PR url spelling Review round 5. gh's "error parsing owner value" prefix also fronts its GH_HOST-mismatch diagnostic — an environment problem, not a missing repository — so the structural classification is narrowed to the two wordings that really mean no usable GitHub remote, and the mismatch stays a transient failure that never retires bindings. PR identity now also recognizes the `.diff` and `.patch` spellings GitHub serves, so such a binding receives its fetched issues instead of a permanent empty snapshot. And the write under a binding's own url no longer waits for a successful issue lookup: during an outage the list query's state still lands on a non-canonically spelled binding instead of being dropped by the sidecar's canonical gate. Tests: the GH_HOST diagnostic stays `failed`, the three structural wordings converge, `.diff` and `.patch` bindings keep their fetched issues alongside the `/files` spelling, and a `www.`/`/files`-spelled open binding turns merged while the lookup is failing. |
||
|
|
1db18fefdb |
fix(hooks): close four trust-boundary holes in hook execution (#10427)
* fix(hooks): close four trust-boundary holes in hook execution HTTP hooks never follow redirects (a 3xx is a non-blocking failure and the target is never contacted). A workspace may narrow the HTTP-hook URL whitelist but never replace one set at User, System, or SystemDefaults scope. Qwen-internal secrets are never substituted into settings, hook URLs/headers, or channel configuration, compared case-insensitively for Windows, and the external-tool guard token joins the denylist; the CLI's duplicate settings resolver is replaced by a core subpath export so the serve fast path stays light. Project-level skill and subagent side effects (allowedTools grants and frontmatter hooks) require a trusted folder, mirroring the settings-file project-hooks gate. Replaces #8396. * fix(hooks): re-apply skill side effects when trust is granted mid-session The Skill tool's "already loaded" dedup return sat above the new trust gate, so a project skill first invoked in an untrusted folder kept its allowedTools and hooks unapplied for the whole session even after the folder was trusted. The gate is now re-evaluated on every invocation; both grants dedup, so re-applying is idempotent. Also keep an own `__proto__` variable in the sanitized child env (spread and delete instead of an assignment loop), correct the guard-token note in the denylist rationale (the ACP child never receives it), and pin the settings-layer and serve fast-path secret refusal with tests. * fix(hooks): give the project-skill trust gate a removal side The gate on a project skill's side effects was enforced only when they were applied: once a repo-supplied skill had registered its frontmatter hooks and granted its allowedTools as session allow rules, a folder trust revoked mid-session (an IDE trust notification flips it live) left both active until the session restarted. Rather than tracking which skills applied what and unregistering on a trust event, the grants are marked trust-gated and the two consumers re-read folder trust at decision time: the hook event handler skips a trust-gated session hook at fire time while the folder is untrusted, and the permission manager leaves a trust-gated session allow rule out of every decision (and the effective-rules listing) while it is. Trust granted again restores both, symmetric with the mid-session grant the gate already honoured, and nothing changes on rounds that registered no project-skill hook. The per-invocation re-application of side effects also turned two WARN lines into the normal steady state of a trusted skill — a dedup returning zero and a hookless skill — so both are debug-level now, reworded to say what the zero means. * fix(permissions): let an ungated same-raw grant clear the kept entry's trust gate The session-allow dedup kept the first entry's trustGated flag, so a user-level grant of a raw the project skill had already granted would inherit the repo grant's suspension when folder trust is revoked. The dedup now keeps the wider grant: an ungated arrival clears the flag; a gated re-arrival (skill reload) stays an idempotent skip and never re-gates a rule the user holds. Also pins the slash-command path's grant payload in the SkillCommandLoader trusted-folder test to exactly { trustGated: true }, so shipping the grants ungated reddens the suite instead of passing on call counts alone. --------- Co-authored-by: qwen-code-dev-bot <[email protected]> |
||
|
|
df6c361dd2 |
fix(review): readable bilingual disclosures for lint deferrals and caller prose (#10587)
* fix(review): readable bilingual disclosures for lint deferrals and caller prose The review body's disclosure sentences carried two readability defects, both visible on PR #10567's posted round-1 body: - The deferred-checker line stuttered: script-lint's deferral/skip reasons ended with an "— not linted" tail written for a standalone context, the gate spliced them under a "the executable-script lint —" prefix, and the body wrapped the result in a sentence that already opens "Not linted:". Posted: "Not linted (tool limitation, not a blocker): the executable-script lint — ... — not linted." The reasons drop the tail, and the disclosure drops the circular prefix — the wrapper names the fact once, the path and reason carry the rest. - The body's Chinese half presented untranslated English as its translation. Two legs: - The deferral disclosure is machine-built from the report, so it can carry a real translation: `scriptLintGate` now returns bilingual disclosure pairs, the report schema gains an optional `reasonZh` (the actionlint deferral supplies it), and an older CLI's report without one falls back to the English reason in both halves. - Caller-prose "Not reviewed" entries stay untranslatable by construction, and the Chinese label now says so — `未审查(原文为英文):` — instead of presenting an all-English sentence as a translation; the payload keeps its own English full stop rather than closing an English sentence with `。`. * test(review): pin the bilingual deferral disclosures the review asked for Five review-suggested pins, each verified by re-running its witness mutation: - The comment-grammar fixture now carries a malicious reasonZh, so the stripCommentGrammar(d.reasonZh) leg is exercised against a live marker (deleting the call previously survived the suite). - The pipeline reasonZh is pinned to its Chinese literal — a toContain('actionlint') fragment was satisfied by the English reason too. - Both skipped reasons get not.toContain('not linted') pins; re-appending either tail previously shipped green. - A two-entry deferred fixture pins the en '; ' and zh ';' joins and the reasonZh-carrying branch end-to-end — every prior fixture held one entry, so no join separator was ever observable, and the only full-sentence zh pin exercised the English-fallback branch. |
||
|
|
3f71359ea8 |
fix(serve): classify channel initialization timeouts (#10404)
* fix(serve): classify channel initialization timeouts * fix(serve): scope the init-timeout safe-retry contract to plain creation The initialize-timeout mapping in sendBridgeError promised retryable:true / sideEffectPossible:false for every route, but two paths mutate before or around the initialize handshake: - POST /session with branch/worktree runs createBranch (moving the shared HEAD) or creates a worktree before spawn, and the rollback on failure is best-effort — a failed checkout rollback leaves the repo on the new branch while the response claims no side effect. - POST /session/:id/branch and /side-task can time out on a replacement channel's initialize after the fork was already durably committed; a contract-trusting retry would commit a duplicate fork. The safe-retry shape is now emitted only when the caller asserts via the new initPrecedesMutations context flag that initialization strictly precedes every durable mutation — POST /session sets it only when no branch/worktree was prepared. All other paths keep the typed init_timeout code, phase, and timeoutMs but omit Retry-After, retryable, and sideEffectPossible, reporting an unknown outcome. The protocol doc narrows the authoritative claim accordingly and notes that timeoutMs reflects the configured --initialize-timeout-ms budget (values shown are the default). Tests pin the branch-body reduced shape (red without the route guard) and the non-initialize label falling through to the generic 500. * docs(serve): clarify init_timeout 504 on load/resume routes The restore Errors list previously described every 504 as session_restore_timeout (retryable, fenced), but an init-budget expiry during ensureChannel returns init_timeout without Retry-After, retryable, or fence — the restore was never dispatched. Add the init_timeout 504 entry and narrow the "any other route" paragraph to mutation-bearing routes, explicitly calling out load/resume. * fix(cli): remove duplicate BridgeTimeoutError import in error-response The file imported BridgeTimeoutError from both @qwen-code/acp-bridge/status and ../acp-session-bridge.js, which caused a TS2300 duplicate identifier build failure. Drop the bridge/status import; the local acp-session-bridge re-export is the one used across the route error handling. * fix(cli): reconcile init_timeout tests and docs with merged #10268 behavior Update error-response and server tests plus the qwen-serve protocol doc to reflect that `newSession` dispatch timeouts now map to a retryable `init_timeout` 504, while `initialize` timeouts without caller context use the reduced contract. |
||
|
|
a8e0d293fb |
fix(core): make loop detection result-aware for task_list polls (#9492)
* fix(core): make loop detection result-aware for task_list polls (#9450)
Identical task_list arguments do not imply an identical result: teammates
mutate the shared task board between calls. The argument-only loop guards
falsely halted polling teammates with 'duplicate tool-call loop detected'
while the board kept changing.
Record executed tool results into LoopDetectionService (agent runtime after
processFunctionCalls; main-session continuations via functionResponse callId
pairing) as privacy-safe SHA-256 fingerprints. For the stateful read tool
(task_list only), the consecutive-identical guard, the global-duplicate
heuristic, the adaptive cap stuck signal, and action stagnation now require
the observed results to be unchanged too. Missing result evidence fails safe
and keeps the pre-fix behavior, preserving the DashScope #5019 protection.
Loop stops become attributable: ReasoningLoopResult carries the exact
LoopType, the interactive stop message and the headless FINISH event /
telemetry completion record include it.
* fix(core): report result-aware loops to arena
* fix(core): complete loop attribution plumbing for #9450
- R1-1: declare loop_type on SubagentExecutionEvent and serialize it in
the QwenLogger sink; previously the spread in agent-headless.ts was
dropped at construction (excess-property spread), so LOOP_DETECTED
stops reached the journal unattributable.
- R1-2: reset loopType at the top of AgentHeadless.execute() so a
re-executed instance (stop-hook continuation, resident turns) never
carries a stale loop attribution into an ERROR/FINISH record.
- R1-12: pair only stateful read tools in requestByCallId — every other
tool is rejected by recordToolResult anyway, and the write-only entries
pin full args objects (write_file contents) up to the eviction cap.
- R1-3: drop the dead 'key' field from requestByCallId entries; the
consumer reads only name/args and recordToolResult recomputes the key.
- Tests: assert loop_type in the telemetry completion record (R1-5) and
pin the re-execution attribution reset.
* test(core): pin result-aware loop guards against silent regressions (#9450)
- Extend the changed-results global-duplicate phase past GLOBAL_DUPLICATE_THRESHOLD rounds so an args-only mutant cannot hide.
- Add heuristic-gate tests that Retry and reset() clear result-aware pair counts (cross-prompt and replay false positives).
- Add the adaptive-cap positive-side test: an interleaved frozen poller halts with TURN_TOOL_CALL_CAP just past the soft cap.
- Add the streak-move fail-safe test: a resumed streak with insufficient fresh result evidence halts instead of trusting stale evidence.
- client.test.ts: drive sendMessageStream with paired ToolResult ids — frozen board halts with global_tool_call_duplicate, changing board keeps running.
- agent-headless: interleaved frozen task_list polling halts via the result-time guard with FINISH loopType attribution.
- agent-interactive: LOOP_DETECTED results surface the exact detector in the stop message and lastRoundError.
* test(core): pin loop_type journal mapping and stateful callId guard (#9450)
* fix(core): keep result-aware loop guards alive for persisted oversized results (#9450)
Oversized results are rewritten into persistence stubs whose envelope embeds
a per-call unique file path (<toolResultsDir>/<callId>.txt), so fingerprinting
the whole model-visible response made every fingerprint unique and silently
disabled every result-aware guard for exactly the largest results: a frozen
task_list board over the persistence gate was never halted. Fingerprint the
semantic payload instead — strip the stub envelope and hash the
preview/truncated content that follows the stable marker.
* refactor(core): keep loop-guard stub parsing in lockstep with the stub builder (#9450)
* fix(core): fingerprint the full output in persistence stubs (#9450)
* test(core): cover unwrapped and truncated oversized stub shapes (#9450)
* fix(core): fingerprint the full output in truncated stubs (#9450)
truncateAndSaveToFile's stub shape carried no full-output digest (unlike
buildStub), so a shared board mutating in the truncated middle band
fingerprinted identically on every poll and the result-aware guards
halted a productive poller. Embed the sha256 of the full pre-truncation
output in both the wrapped and the unsaved (disk-failure) shapes, and
make the loop guards' stub parser prefer the digest in every shape
before falling back to the visible payload.
* fix(core): fingerprint batch-budget fits for the loop guards (#9450)
The batch-budget finalizer's fitText header embeds a per-call artifact
path, so every oversized batch-budget result fingerprinted uniquely and
the result-aware loop guards were silently disabled for exactly those
results. Embed the sha256 of the full pre-fit text in the header (right
after the constant prefix so tiny allocations that slice the header
still keep it); the guards' digest-first stub parsing picks it up.
* fix(core): make stateful pair counts order-aware (#9450)
The result-aware (repeat key, result fingerprint) counter accumulated
turn-wide, so a board oscillating between two byte-identical states
reached GLOBAL_DUPLICATE_THRESHOLD on one state and halted a poller
whose every result differs from its predecessor — contradicting the
recorded invariant that a changed result is observable progress. Count
consecutive identical results per key instead: interleaved frozen polls
still accumulate, but any result differing from the key's predecessor
restarts the count.
* fix(core): make the alternating-pattern guard result-aware (#9450)
checkAlternatingPattern still counted task_list args-only at request
time, so a poller alternating task_list with another call (the ABAB
shape of check-board-do-work) was halted on the first full window even
while the board kept changing. Apply the same result-aware carve-out
the sibling detectors got: a stateful participant's rolling results are
checked when the window fills, any changed result restarts the window,
and missing result evidence fails safe into the argument-only halt.
* fix(core): disarm the cap on thawed boards and anchor stub digests (#9450)
Two result-aware guard hardenings from review:
1. The adaptive cap's stateful stuck signal no longer latches a high-water
peak. recordToolResult now feeds the cap a statefulCapKeyRepeat that
falls back to the keys' current streaks when a result changes, so a
frozen-then-thawed task board releases the cap exactly as it releases
the result-time global-duplicate count. A permanently frozen board still
arms the cap and halts just past the soft cap.
2. stripPersistenceEnvelope no longer honors the FULL_OUTPUT_DIGEST_LABEL
marker anywhere in a string. Stub recognition is gated on the producer
prefixes (persisted-output / output-too-large / truncated / batch-budget
fit) and the digest must be line-anchored with a full 64-hex payload, so
board content that merely quotes a stub's digest line is fingerprinted
verbatim instead of collapsing to (or varying with) the quoted window.
The batch-budget fit prefix is now an exported constant
(BATCH_BUDGET_FIT_PREFIX) so the parser shares the producer's literal.
extractToolResultText / fingerprintToolResult / isStatefulReadTool are
exported so the daemon turn-loop guard fingerprints results identically
(issue #9450 requirement #6).
* fix(core): count deduped provider call ids once in the subagent loop guard (#9450)
Request counts and result evidence were fed from different populations: the
consecutive-identical request counter counted every streamed function call
(pre-dedup), while results land once per deduped executed call. A
provider-emitted duplicate call id — the exact pathology dedupeToolCallsById
exists for — left the request counter one ahead of the result evidence, so
the result-aware exemption failed safe and halted a fully productive
task_list poller.
The subagent stream loop now feeds the loop guard one event per call id per
attempt (a per-round Set mirroring dedupeToolCallsById; id-less calls are
never deduped), keeping request counts and result evidence on the same
population. Cleared on retry alongside the accumulated function calls.
* fix(core): surface subagent loop attribution in the FINISH display (#9450)
AgentFinishEvent.loopType was write-only: agent-headless emits it on every
loop-halted run, but no production code read it, so the FINISH surface
collapsed every loop stop into the generic LOOP_DETECTED label. AgentTool's
FINISH handler now reads loopType and appends it to the failed task card's
terminateReason (the same `(loopType)` attribution agent-interactive already
renders); the journaled sink remains SubagentExecutionEvent.loop_type.
* fix(cli): make the daemon loop guard result-aware for task_list polls (#9450)
The daemon/ACP mirror (recordDaemonToolCalls) still counted task_list
args-only at request time, so the #9450 false positive survived on that
runtime: a daemon-served leader polling task_list with identical arguments
while peers kept completing tasks hit the global-duplicate mirror at the 6th
request (skipLoopDetection=false) or the adaptive cap's stuck signal past
the soft cap, even though every executed result differed. Issue #9450
requirement #6 asks main-session, subagent, and ACP paths to behave
equivalently where they support the same tool.
recordDaemonToolCalls now skips stateful read tools at request time, and a
new recordDaemonToolResult records each executed result post-execution
(wired where executed results are queued), keyed on (call, result
fingerprint) via the shared fingerprintToolResult. It feeds the cap's stuck
signal (statefulMaxResultRepeat, disarmed on a changed result, mirroring
core's statefulCapKeyRepeat) and a result-time global-duplicate count
(gated on skipLoopDetection, mirroring core's recordToolResult). Batch loops
and the final finalize observe loopState.loopDetected so a result-time
detection stops the turn.
* fix(core): count deduped provider call ids once in the main-session loop guard (#9450)
Main-session twin of the subagent fix (
|
||
|
|
8cdb55386a |
fix(core): recover model requests rejected with HTTP 413 via one-shot compaction (#10408)
* fix(core): recover model requests rejected with HTTP 413 via one-shot compaction When an OpenAI-compatible endpoint sits behind a reverse proxy with a request-body byte limit, a session below the token-based auto-compaction threshold can still have its serialized request rejected with a bare HTTP 413 (often an HTML error page). The token-wording overflow detector never matched it, reactive compression never fired, and every subsequent prompt re-sent the same oversized history, leaving the session permanently broken (#10380). - Add a model-request-scoped 413 detector (utils/request-payload-error.ts) consulted only by llm-chat's send catch, so upload/file 413s are never misclassified as context overflow. - Route a payload-overflow 413 into the existing one-shot reactive compression path and retry once. - Truncate oversized tool-result/text payloads in the compaction side-query input on this path so the side-query itself fits under the same gateway byte limit. - Surface an actionable "start a new session" error when recovery cannot shrink the request under the limit, preserving the HTTP status. Co-authored-by: Qwen-Coder <[email protected]> * fix(core): suppress post-compact restoration on payload-overflow compaction Restoration re-embeds full-size image payloads and file blocks that the slimmed 413 side-query never carried, re-inflating the rebuilt retry request back over the gateway byte limit. On the requestPayloadTooLarge path compose post-compact history with maxFiles/maxImages=0 so the retry is dominated by the summary that just fit. Co-authored-by: Qwen-Coder <[email protected]> * fix(core): keep part siblings and slim functionCall args in 413 slimming Truncated text parts were rebuilt as bare { text }, dropping sibling properties such as thought/thoughtSignature that the converter pipeline keys reasoning content off. Spread the part instead. Also truncate oversized top-level functionCall string args (write_file/edit carry whole file contents there) on the payload-overflow path, matching what estimatePartChars already bills. Co-authored-by: Qwen-Coder <[email protected]> * fix(core): keep cause-wrapped 413 status on the actionable error Detection walks the .cause chain five levels, but the status copy onto the surfaced actionable error used the shallow top-level lookup, so a cause-wrapped 413 lost its .status and downstream bucketing recorded unknown. Expose the deep-found status on RequestPayloadTooLargeInfo and reuse it when copying. Co-authored-by: Qwen-Coder <[email protected]> * fix(core): give 413 recovery outcome-accurate advice instead of unconditional /clear The one-shot payload-overflow wrap fired the same "start a new session (/clear)" error for every non-COMPRESSED outcome, including two outcomes where that advice is false (#10380): - Transient compaction failure: a side-query 504/reset made compress() throw, the catch swallowed it, and the wrap advised /clear — but reactiveCompressionAttempted is per-send, so the next prompt gets a fresh one-shot and may recover. Keep the original 413 instead so the next send can retry recovery. - NOOP compaction: no earlier history to compress means the oversize sits in the current request itself; /clear + retry reproduces the identical failure. Surface a "reduce the current request" variant instead. Only the genuine "compaction ran and still did not fit" outcome keeps the new-session advice. Adds the NOOP message constant and two regression tests that fail when the outcome split is reverted. Co-authored-by: Qwen-Coder <[email protected]> * fix(core,cli): report payload_overflow trigger reason for 413-driven compactions A successful 413-driven compaction reported triggerReason 'token_limit' (the reactive path passes trigger 'auto' and only 'image_overflow' was ever upgraded), so the CLI notice claimed the conversation "approached the input token limit" and the recorded payload logged a fabricated window-sized token count — contradicting the very premise that 413 fires below the token threshold (#10380). Add a 'payload_overflow' member to CompactionTriggerReason, set it in compress() when opts.requestPayloadTooLarge, and give the CLI notice a matching clause ("exceeded the endpoint request-body limit"). The reactive 413 path is force=true, so the screenshot-trigger upgrade in the non-forced gate cannot overwrite the reason. Covered by a service test asserting the COMPRESSED result's triggerReason and a hook test asserting the notice wording; both fail when the fix is reverted. Co-authored-by: Qwen-Coder <[email protected]> * fix(core): label and log the 413 recovery path accurately for oncall The new recovery path was silent or mislabeled exactly where an incident needs it (#10380): - No log at the decisive wrap transition: add a warn when recovery is exhausted and the actionable 413 error is surfaced. - The two adjacent warns still said "context overflow" on the HTTP 413 payload-overflow path; split them on requestPayloadOverflow.isTooLarge the way the first branch already does. - stats.textPartsTruncated was written but never logged, and the runColdCompression slimming log was gated on images/documents being non-zero, so text-only slimming (the typical 413 case) logged nothing; include it in both the condition and the message. Logging-only change; no behavior impact. Co-authored-by: Qwen-Coder <[email protected]> * fix(core): anchor 413 recovery accounting on a local history estimate A bare HTTP 413 carries no provider token counts, so the reactive originalTokenCount fell through to the full context window. compress() then anchored its newTokenCount math on that window and stamped the post-compaction count at roughly window minus visible history — orders of magnitude above the real size. The inflated count persists into the next turn, force-re-compacting the just-compacted history or false-tripping a configured sessionTokenLimit right after a successful recovery (#10380). On the payload-overflow route, anchor on estimateContentTokens over the actual history — the same estimator the missing-usage accounting path already uses — instead of the window. Provider-reported counts (actualTokens / limitTokens) still take precedence, and the token-wording overflow path keeps its existing projection. Witness: new llm-chat test asserts the reactive compress() call carries an estimate far below the window; it fails (200000 >= 10000) when the anchor reverts to the window fallback. Co-authored-by: Qwen-Coder <[email protected]> * fix(core): keep 413 recoveries off the cache-sharing side-query path The cache-sharing request is built from the UNSLIMMED history, but a payload-overflow compaction exists because that exact payload was just rejected at the gateway byte limit. On mixed errors (413 status plus provider-reported counts that fit the window) canShareCache flips true and re-uploads the rejected payload at full size, takes another 413, logs a misleading cache-sharing failure, and only then recovers via the slimmed cold path — a wasted round-trip and a hole in the byte-limit invariant the slimming exists to enforce. It widens further now that the 413 anchor is a truthful estimate (sharedRequestFits flips true for every 413 compaction). Add !opts.requestPayloadTooLarge to canShareCache so payload-overflow recoveries use the slimmed cold path exclusively (#10380). Witness: cache-sharing fixture where every other conjunct holds; compress() with requestPayloadTooLarge must never call generateText. Removing the conjunct makes the cache-sharing call reappear (red). Co-authored-by: Qwen-Coder <[email protected]> * test(core): pin the maxFiles half of payload-overflow restoration suppression The suppression test added for #10380 builds its history exclusively from screenshot payloads, so only the maxImages: 0 wiring can turn it red; mutating maxFiles back to tuning.maxRecentFiles kept every PR test green. Large sessions are dominated by read_file blocks (~20KB each), so a regression on the file half re-inflates the rebuilt retry request past the same gateway byte limit — a second 413 after recovery was already achievable. Add a sibling test with a real read_file result on disk: on the token-driven path the file content IS re-embedded (live comparator), and with requestPayloadTooLarge the serialized post-compact history carries no file-restoration block. Goes red when maxFiles reverts to tuning.maxRecentFiles. Co-authored-by: Qwen-Coder <[email protected]> * test(core): cast the restoration-suppression fake config to Config Spreading the mocked Config loses its member types, so the baseOpts config no longer satisfied CompressOptions after the getTargetDir override. Restore the Config type with an assertion. Co-authored-by: Qwen-Coder <[email protected]> * fix(core): handle payload-overflow compaction edge cases Treat returned compression API failures like transient side-query failures so a recoverable 413 does not receive permanent new-session advice. Keep payload-overflow token accounting off the slimmed side-query usage branch, and avoid splitting surrogate pairs when trimming request text for compaction. Co-authored-by: Qwen-Coder <[email protected]> * test(core): make 413 compaction fixtures carry a realistically large visible history The payload-overflow tests mocked a 90K/180K-token payload over a ~14-token visible history, so the estimate-based accounting (which 413 recoveries now use) correctly flagged the summary as an inflation. Give the fixtures large visible content, matching the real shape of a gateway-rejected payload, so they verify trigger reason and cold-path routing against a genuine reduction. Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
834b66d209 |
fix(java): read control response subtype from payload (#9924)
* fix(java): read control response subtype from payload Use the parsed control response wrapper when deciding whether a CLI control response is an error, matching the nested wire format and preserving the legacy top-level fallback. Co-Authored-By: Claude Sonnet 4.6 [email protected] * fix(java): keep sendPrompt stream aligned after control errors Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * test(java): align control error fixture Use the real control-response error envelope in session tests so the subtype handling coverage matches the CLI wire format. Co-Authored-By: Claude Sonnet 4.6 [email protected] * fix(java): simplify control response subtype lookup Co-Authored-By: Claude Sonnet 4.6 [email protected] * test(java): cover control response warning logs Co-Authored-By: Claude Sonnet 4.6 [email protected] * test(java): strengthen control response subtype coverage Cover missing response payloads, non-error subtype boundaries, and warning payload content so subtype detection regressions are observable. Co-Authored-By: Claude Sonnet 4.6 [email protected] * test(java): pin control response subtype boundaries Co-Authored-By: Claude Sonnet 4.6 <[email protected]> --------- Co-authored-by: zhangyu.34 <[email protected]> Co-authored-by: Claude Sonnet 4.6 <[email protected]> |
||
|
|
d9a120a105 |
fix(core): show edit diffs when a PreToolUse hook returns ask (#9441)
* fix(core): show edit/exec diffs when a PreToolUse hook returns ask When a PreToolUse hook returns an 'ask' decision, the tool was bounced back to awaiting_approval with a synthetic plain-text prompt carrying only the hook reason, so Edit/WriteFile confirmations lost their diff view. Reuse the tool's own confirmation view (edit diff / exec command) when it provides one, attaching the hook reason via a new hookAskReason field rendered above the body. Tools without a structured view keep the plain reason prompt. Fixes #9434 * fix(core): preserve hookAskReason when restricting bubbled edit/exec confirmations * fix(core,cli): close PreToolUse ask-bounce abort/rendering gaps (#9434 follow-up) * test(core): pin hookAskReason passthrough in the approval restriction * fix(shell): keep a user-confirmed sed edit across a re-entrant preview The PreToolUse ask bounce re-fetches getConfirmationDetails, which re-ran prepareSedEdit and unconditionally cleared confirmedSedNewContent, discarding the content the user already edited and approved. Only clear it when the freshly prepared edit differs from the previously prepared one (a genuinely new sed target). * fix(core): re-apply plan-shell policy in the PreToolUse ask bounce The bounce built its confirmation from the tool RAW details, so hideModify/skipIdeDiff/warnings and validatePlanModeShellApproval were dropped. Decorate the bounced details with the plan-shell decision and wrap onConfirm to validate at approval time, mirroring the ordinary confirmation phase. * fix(core): close round-4 ask-bounce findings (#9434) Carry the confirmation-phase plan-shell decision into the execution phase via a per-callId map so the PreToolUse ask bounce compiles and re-applies decoratePlanModeShellConfirmation / validatePlanModeShell Approval (R4-1, R3-2). Re-enter the captured invocation context around the bounced onConfirm so the tool-invocation guard sees the invocation's identity, not the responder's (R4-4). Collapse the two inline cancel-before-execution copies into cancelWithSyntheticResponse (R3-5) and correct the bounce docstring (R3-4). shell: keep a prepared/confirmed sed edit authoritative when a re-entrant preview fails instead of flipping to raw execution (R4-3), and present the retained confirmed content in the re-confirmation view so it matches what approval writes (R4-2). cli: render hookAskReason on the stream-json permission suggestions and the ACP permission request content (R3-3). Tests: hook-reason cap on short terminals (R3-7), exec-branch hookAskReason passthrough (R3-8), abort-resolves and reason-fallback bounce variants (R3-9), sed retention across a re-entrant preview (R4-5), structured-bounce decline (R4-6), plan-shell bounce through the scheduler (R4-7). * fix(core): close round-5 ask-bounce findings (#9434) * fix(core): close round-6 ask-bounce findings (#9434) R6-1: a bounced re-execution skips the PreToolUse hook, so the confirmation surfaces while bounced must not change what runs: - the IDE diff is no longer opened on a bounce; it answered through the pre-wrap details, bypassing dropBounceModifyPayload - ModifyWithEditor is rejected while bounced (the call stays in awaiting_approval; Cancel/Proceed still recover) - the hook-reviewed request.args snapshot is restored before the bounced re-execution, closing the stream-json responder channel that mutates request.args directly before answering R6-2: the bounced plan-shell branches now use the ordinary confirmation phase's planShellResponseClaimed guard (checked and set synchronously before the validation await), so racing answers to the same bounced call collapse to one handleConfirmationResponse. * fix(core): invalidate round-1 IDE diff on PreToolUse ask bounce (#9434) A round-1 openDiff resolver that survives into a bounced round (sibling auto-approval never closes the diff) could answer the bounced confirmation through the status-only awaiting_approval guard, bypassing the bounced onConfirm protections (hideModify, dropBounceModifyPayload, claim guard). Bounce now bumps a per-call confirmation epoch and resolves/closes the outstanding round-1 IDE diff the same way the CLI's handleConfirm does; openIdeDiffIfEnabled drops its answer when the epoch has moved on. Adds accept/reject arm regression tests. (#9434 review R7-1) * fix(core): close round-8 ask-bounce findings (#9434) - R8-1: pass hookAskReason through the info branch of restrictWorkflowConfirmationDetails, mirroring edit/exec, so bubbled info approvals keep the hook reason in the leader UI (+ sentinel test). - R8-2: the ask bounce no longer awaits resolveDiffFromCli on the critical path (closeDiff is bounded only by the 10-minute IDE RPC timeout and takes no signal); the invalidation is fire-and-forget and an abort re-check precedes the status transition. Epoch invalidation still drops stale round-1 answers (+ regression test with a hung RPC). * test(core): remove duplicate scheduler stub * test(cli): colocate permission-suggestions test with its module The test lived in src/utils/ and imported ./permission-suggestions.js, but the implementation is src/nonInteractive/permission-suggestions.ts, so vitest collection failed with TS2307 and the tests never ran. Move the test next to the module per house convention; no test-body changes needed. * fix(core): harden PreToolUse ask bounce confirmations - Forward the confirmation payload's cancelMessage through the bounced edit onConfirm wrapper (the info fallback and the pre-PR synthetic prompt both forwarded it), so a stream-json host denial keeps its reason instead of falling back to 'User did not allow tool call'. newContent stays excluded: bounced confirmations set hideModify, so the modify channel must not rewrite hook-reviewed content. - Refuse round-1 IDE diff resolutions for calls in bouncedAwaitingApproval: a bounce re-enters awaiting_approval, which let a stale openDiff resolution answer the bounced confirmation — the accept path flowed panel content through _applyInlineModify (type 'edit', hideModify not checked there) and executed it on the hook-skipping re-execution. Adds regression tests for both; the IDE test goes red when the guard is removed and the cancel-reason test goes red when forwarding is dropped. Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
bd59085311 |
fix(web-shell): clear copy-feedback reset timers on unmount (#10655)
Copy buttons flip to a check mark and schedule a bare setTimeout(() => setCopied(false)) to reset it. When a test file's environment is torn down within that window the callback throws ReferenceError: window is not defined out of react-dom's state dispatch, and since the unit suites fail on unhandled errors an all-green run exits 1 — main's own Test (ubuntu) hits this through AssistantMessage.test.tsx. A new useCopiedFlash hook owns the transient flag: flash() sets it and schedules the reset, a re-flash restarts the window instead of letting the older timer cut the newer feedback short, and unmount clears the pending timer. The six call sites with the bare pattern (assistant, system and timestamp messages, both Markdown code-block buttons, the Git log dialog) adopt it; SessionOverviewPanel's SessionIdCell already carried the cleanup. Two tests pin the cleanup via vi.getTimerCount() after unmount and one pins the restarted window. |
||
|
|
f86098363d |
test(core): pin tools.eager Object.prototype-key tolerance and correct related docs (#10479)
* test(core): pin tools.eager Object.prototype-key tolerance and correct related docs Item 1 of #10400 (a tools.eager entry named after an Object.prototype key crashing PermissionManager.initialize) was already fixed before #10098 merged: resolveToolName only reads own properties of the alias table and the crashing unknown-entry loop was removed. Pin that contract with regression tests so a reintroduction fails CI: - initialize() tolerates constructor/toString/valueOf/... eager entries - resolveToolName returns Object.prototype-keyed names unchanged - config.test.ts: add the third LS x tools.eager cell (enabled and covered by the list -> registered eagerly) Also correct the doc inaccuracies tracked in the issue: - settings.md: permissions.deny blocks MCP tool calls at runtime but does not remove them from the registry (tools.disabled / the per-server excludeTools filter does) - ToolSearch-absent caveat (5 copies): note the resumed-session history re-reveal and tools.visible carve-outs - sdk-typescript docs + README: note the tools.eager exempt families Co-authored-by: Qwen-Coder <[email protected]> * test(core): cover visible permission-deferred tools Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
6828ac7829 | fix(cli): detect overlapping review ranges (#9801) | ||
|
|
442951afee |
fix(web-shell): restore composer after closing MCP plugin panel (#10232)
* fix(web-shell): restore composer after closing MCP plugin panel Keep MCP status data from independently marking the chat composer as blocked after the Plugins panel closes, and cover the regression with an App test. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * test(web-shell): guard storage cleanup consistently Wrap both sessionStorage and localStorage cleanup so storage-disabled test contexts do not fail before the MCP dialog regression can run. Co-Authored-By: Claude Sonnet 4.6 [email protected] * docs(web-shell): explain MCP dialog gating Co-Authored-By: Claude Sonnet 4.6 [email protected] --------- Co-authored-by: zhangyu.34 <[email protected]> Co-authored-by: Claude Sonnet 4.6 <[email protected]> |
||
|
|
2f2e953fca |
fix(tools): treat blank task_list filters as absent (#10159)
* fix(tools): treat blank task_list filters as absent (#9281) Blank (empty or whitespace-only) optional owner/blockedBy params on task_list activate as filters because listTasks() activates every filter whose value is !== undefined: blockedBy: '' filters out every task ("No tasks found."), and owner: '' fails with "Cannot filter by owner: owner must include at least one letter, number, or hyphen." Both contradict the tool's own presentation — the schema describes these params as optional and getDescription() only shows truthy filter values. Normalize blank owner/blockedBy to "no filter" in TaskListInvocation.execute() before calling listTasks(). Non-blank owner values that sanitize to nothing (e.g. "!!!") keep the existing error, and the store-level listTasks() !== undefined contract stays untouched. Tests: red-to-green coverage for the four blank-filter shapes plus a regression test for exact non-empty blockedBy filtering. Co-authored-by: Qwen-Coder <[email protected]> * fix(core): normalize task dependency filters * fix(core): treat blank blockedBy filters as absent in task_list Blank (empty/whitespace-only) blockedBy values normalized to '' and were forwarded to listTasks, where the filter activates on `!== undefined` and `includes('')` never matches — so a populated board reported "No tasks found." (the exact bug #9281 set out to fix). Blanks now stay undefined, the store's absent marker. A non-blank value that normalizes to nothing after the '#' strip (a bare '#') now fails closed with an explicit error, mirroring the owner path, instead of silently activating a never-matching filter while getDescription() still advertises blockedBy=#. The trim + '#' strip moves into a shared normalizeTaskId helper next to assertValidTaskId in tasks.ts; its result is still routed through assertValidTaskId before use. Aligning task-update's ID parameters on the helper is left as a follow-up. Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
843d991a87 |
feat(serve): add sessionless POST /language for user-level language sync (#10554)
* feat(serve): add sessionless POST /language for user-level language sync The daemon only exposed language synchronization through POST /session/:id/language, which requires an existing owned session — a host on a welcome page (zero sessions) had no supported way to sync the user-level preference, and creating a session does not repair a stale global output-language.md. Add a process-global POST /language route (issue #10234): the daemon process persists general.language / general.outputLanguage at user scope and writes the global output-language.md as the single writer, then fans a new sessionless qwen/control/user/language ext-method out to every trusted runtime with a live channel (i18n switch + settings reload + session system-instruction refresh). Project-bound output-language files keep their override; zero sessions is a success. Advertised as the conditional user_language_sync capability (same persistSettingAvailable predicate as workspace_settings), with a typed DaemonClient.setUserLanguage for hosts. Closes #10234 * codex: address PR review feedback (#10554) Co-authored-by: Qwen-Coder <[email protected]> * test(daemon): Strengthen user language sync contracts Co-authored-by: Qwen-Coder <[email protected]> * codex: address PR review feedback (#10554) Co-authored-by: Qwen-Coder <[email protected]> * codex: address PR review feedback (#10554) Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
358efdceeb |
test(ci): stabilize shared-runner budgets and cron interactive checks (#10648)
* test(core): use CPU time for shell safety budgets Co-authored-by: Qwen-Coder <[email protected]> * test(integration): stabilize cron priority assertion Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
be585888f8 |
fix(serve): add opt-in prompt-settled close grace for poll-based SSE clients (#10344)
* fix(serve): fix event_stream_resync_required for poll-based SSE clients Two root causes were identified from production trace data: **Root cause 1 (72% of incidents): ring_evicted** Agentic coding sessions generate 25 000+ events per turn, exhausting the default ring size of 8 000. When the ring fills, the oldest events are evicted and a reconnecting client receives `ring_evicted`, forcing a full resync. Raise `DEFAULT_RING_SIZE` from 8 000 to 32 000 to provide adequate headroom (~1–2 MB RAM per session at typical event sizes). **Root cause 2 (13% of incidents): epoch_reset / session_rebuilt** Poll-based SSE clients (e.g. DataAgent CLI) disconnect for up to 12 s between polls. When the last client detaches immediately after a prompt settles, the daemon closes the session through `last_client_detached`. The next poll reconnects, restores a new session with a fresh EventBus epoch, and the old `Last-Event-ID` becomes invalid, triggering resync. Add `sessionPromptSettledCloseGraceMs` (default 0, preserving existing behavior for all library consumers). When set > 0, a deferred timer replaces the immediate `last_client_detached` close after a prompt result. A reconnecting poll client calls `subscribeEvents`, which clears the timer and keeps the session alive. Web clients with persistent SSE connections are completely unaffected — their `clientIds.size` stays > 0 so `entryIsAutoCloseCandidate` never fires. `qwen serve` exposes `--session-prompt-settled-close-grace-ms` to let operators tune the window. The DataAgent CLI sets 60 000 ms (60 s) to comfortably cover its 12 s poll gap with margin. Fixes: ring_evicted and epoch_reset resync incidents for CLI polling clients. No behavior change for default-config or web shell deployments. * fix(serve): address review criticals — timer overwrite and grace-hold consumed on first reconnect R1-1: schedulePromptSettledClose now clears any pending timer before arming a new one, so two prompt settles inside one grace window no longer leak the first timer and close the session at the first settle's deadline. R1-3: subscribeEvents now calls cancelPromptSettledTimer (timer only) instead of clearPromptSettledClose (timer + stamp), so the promptSettledAt hold in entryIsAutoCloseCandidate survives the subscribe → drain → detach cycle. detachClient calls rearmPromptSettledClose after maybeCloseIdleSession to reschedule the close for the remaining grace window once the last subscriber drops. Also fixes two doc mismatches in bridgeOptions.ts: removes the false claim that qwen serve sets 60_000 (the default is 0, it is opt-in via flag) and corrects the scope note — the hold gates every automatic close path, not just prompt_settled. * fix(serve): narrow PR to prompt-settled grace and revert default ring-size bump Per review feedback, scope the change to the opt-in prompt-settled close grace window and its lifecycle edge cases: - Revert the global DEFAULT_RING_SIZE change from 32,000 back to 8,000; keep only the --event-ring-size option for operators who need it. - Fix deferred-close handling around subscribeEvents, detachClient, closeSession, and killSession so the grace window survives reconnect churn and refused closes without orphaning timers. - Add DAEMON-005 regression tests for the above lifecycle edge cases. - Expose sessionPromptSettledCloseGraceMs in SDK daemon-status types and document the new CLI flag. Refs: #10344 * fix(cli): tolerate yargs line wrap when checking IPv4 help text The new --session-prompt-settled-close-grace-ms flag widens yargs' option column, causing the --hostname description to wrap 127.0.0.0/8 across lines. Strip whitespace before the substring assertion so the test stays robust. |
||
|
|
3a0c4c6108 |
fix(vscode): restore native diff approval after WebShell cutover (#10534)
* fix(vscode): restore native diff approval flow Co-authored-by: Qwen-Coder <[email protected]> * fix(vscode): only accept host-delivered web-shell permission decisions Sandboxed iframes inside the panel webview (MCP apps, artifact previews) can postMessage to the webview window, and the webShellPermissionDecision handler could not tell them apart from extension-host messages. Accept only the platform-delivered shape (event.source === null) so untrusted content cannot vote on pending approvals, mirroring the existing event.source validation used for daemon token messages in the web shell. Co-authored-by: Qwen-Coder <[email protected]> * fix(vscode): open web-shell permission diffs read-only The web-shell approval bridge resolves the daemon permission with an option id only; the edited right-hand content of a native diff is consumed solely by the IDE-mode resolver (IdeClient.diffResponses), which is not populated when IDE mode is off. An editable right side therefore silently discarded anything the user typed before pressing Accept. Diffs opened with source 'web-shell' are now passed through showDiff with readOnly so the proposed side stays locked, while IDE-mode diffs keep the existing writeable-in-session behavior. Co-authored-by: Qwen-Coder <[email protected]> * fix(vscode): align showDiff overload signatures with options argument The two-argument showDiff overload takes options where the implementation expects the optional old-content string; widen the implementation parameter and resolve the options object accordingly. Co-authored-by: Qwen-Coder <[email protected]> * fix(vscode): only vote web-shell approvals from the permission diff qwen.diff.isVisible is also true on the user's original file while a permission diff is open (originalFilePath match), and showDiff opens with preserveFocus, so Ctrl+S or the check/cross title buttons on the original file ran qwen.diff.accept/cancel and the vote loop resolved the pending web-shell approval for an edit the user may never have looked at. Pass the triggering document context into respondToPendingPermission and only take the web-shell branch when the trigger is a qwen-diff document; the legacy pendingPermissionResolve vote path is unchanged. Co-authored-by: Qwen-Coder <[email protected]> * fix(web-shell): lowercase tool names inside isEditToolName isEditToolName was the only is*ToolName helper that required callers to pre-lowercase the name; the next caller following the sibling convention and passing the raw wire name would silently never match, leaving a pending edit row auto-expanding and expandable while its native diff approval is outstanding. Normalize internally like the rest of the family and pass the raw name at the call site. Co-authored-by: Qwen-Coder <[email protected]> * docs(web-shell): note pending-edit-approval exception in expand invariant comment The expandability comment above ToolLine's expandable flag claimed content never controls whether the user can open a row, which the pending-edit- approval gating introduced in this PR falsifies. Extend the comment with the exception so the stated invariant matches the code. Co-authored-by: Qwen-Coder <[email protected]> * test(web-shell): pin allow_once-first ladder for native edit approvals Pin the remaining R1-4 coverage: respondToPendingPermission must submit the allow_once option (never allow_always) when an edit approval carries both kinds, and toPermissionOptions must keep both kinds on edit approvals with allow_always first in wire order — the arrangement that makes the App-side kind preference the only guard against a single native Accept escalating to "Allow All Edits". Swapping the kind ladder turns the App test red (mutant-checked). Co-authored-by: Qwen-Coder <[email protected]> * test(vscode): expect the readOnly options argument in showDiff command tests The read-only permission-diff work added a 4th options argument to diffManager.showDiff in the qwenCode.showDiff command but left two pre-existing assertions expecting the old 3-argument call, turning the package unit suite red. Update both assertions and add a passthrough test pinning that readOnly: true reaches the diff manager. Co-authored-by: Qwen-Coder <[email protected]> * fix(vscode): accept host-relayed permission decisions by their source window The webShellPermissionDecision source guard introduced for R1-1 checked for event.source === null, but extension-host messages never arrive that way: they travel over the webview message port and are relayed into the content frame by the webview preload, which posts them from the parent frame. On a real host the guard therefore rejected every Accept/Reject delivered through the native diff flow. Match the shape the host actually produces (event.source === window.parent) — the same convention the web shell already uses for daemon token messages — which keeps nested sandboxed iframes (MCP apps, artifact previews) out because they post from their own child windows, and fail closed on synthetic deliveries with no source. Co-authored-by: Qwen-Coder <[email protected]> * fix(vscode): only accept diff votes targeting a pending permission diff The fromDiffEditor gate from R1-3 treats any qwen-diff editor as the pending web-shell approval, so while a permission is pending, accepting an unrelated qwen-diff (a stacked approval for another file, or a diff left over from another session) resolved the pending approval for an edit the user never looked at. Ship the paths the web shell is actually waiting on with webShellPermissionState, keep them in sync while the pending flag stays true, resolve them the same way the showDiff command does, and only take the web-shell vote branch when the triggering document matches one of them. The legacy pendingPermissionResolve path is unchanged. Co-authored-by: Qwen-Coder <[email protected]> * test(vscode): pin web-shell diff source to readOnly showDiff mapping Nothing asserted that handleOpenDiff turns source: 'web-shell' into the readOnly option on qwenCode.showDiff, so the lock that keeps permission diffs from silently discarding hand edits could regress unobserved. Pin both directions through the real command registration. Co-authored-by: Qwen-Coder <[email protected]> * fix(vscode): keep read-only approval diffs from reusing writable twins showDiff honored the readOnly option only on the fresh-open path; the reuse paths matched on path+content alone, so a web-shell approval could be deduped onto (or refocused to) a writable IDE-mode diff for the same (path, old, new) triple. Hand-edits typed there are silently discarded when the approving daemon tool applies its own proposed content -- the exact harm readOnly was added to prevent. Include the writability in DiffInfo and require it to match in hasExistingDiff/focusExistingDiff. Co-authored-by: Qwen-Coder <[email protected]> * fix(vscode): clear web-shell permission flag when the chat view disposes webShellPermissionPending was reset on panel dispose, session change and webShellReady, but not when the view-hosted webview was disposed. With an edit approval pending, closing the sidebar chat view left hasPendingPermission() true; an accept from the still-open diff editor then found no active webview, skipped the vote and fell through to the absent legacy resolver, so the vote vanished while acceptDiff had already closed the diff. Reset the flag and paths in webviewView.onDidDispose, mirroring the panel dispose path. Also pin hasPendingPermission() to the webview-pushed state so the extension command gate cannot regress to the legacy-resolver-only check. Co-authored-by: Qwen-Coder <[email protected]> * test(vscode): pin the command-level diff vote gate chain Neither the fromDiffEditor derivation from the diff scheme in qwen.diff.accept nor the hasPendingPermission() precondition in front of it was asserted, so flipping the scheme comparison or reverting the gate to the legacy-resolver check shipped green. Drive the registered command handler with a permission-aware provider stub and assert both links. Co-authored-by: Qwen-Coder <[email protected]> * test(vscode): expect pending:false when permission diffs are torn down The webview->host webShellPermissionState post from closeOpenPermissionDiffs had no test: deleting that post shipped green and would leave WebViewProvider.webShellPermissionPending true after the host unmounts. Assert the teardown posts {pending: false, paths: []} and closes the tracked diff. Co-authored-by: Qwen-Coder <[email protected]> * fix(vscode): complete WebShell host interaction parity Co-authored-by: Qwen-Coder <[email protected]> * fix(vscode): surface a notice when a diff vote is not applied The webShellPermissionDecision chain dropped votes silently when respondToPendingPermission resolved false (catching up after a session switch, approval already resolved elsewhere) or when the shell was not mounted yet: `.catch(handleShellError)` only fires on rejections, so a hung approval was impossible to triage. Show an info notice in both cases — without the hard-error state reset — since false is also the normal outcome of the benign double-vote race. Co-authored-by: Qwen-Coder <[email protected]> * fix(vscode): complete native diff follow-ups Co-authored-by: Qwen-Coder <[email protected]> * fix(vscode): address native diff verification findings Co-authored-by: Qwen-Coder <[email protected]> * test(vscode): cover legacy diff permission fallback Co-authored-by: Qwen-Coder <[email protected]> * fix(web-shell): accept native votes for structured edits Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
67cd326657 |
fix(web-shell): release final footer after background agent completion (#10503)
* fix(web-shell): release footer after agent completion * fix(web-shell): normalize terminal background agents * fix(web-shell): unify background agent terminal projection --------- Co-authored-by: 易良 <[email protected]> |
||
|
|
41cdc4f99a |
feat(external-context): load administrator-owned Mem0 dialects (#10634)
Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
9154671f94 |
fix(cli): preserve existing skill on reinstall rename failure (#10189)
* fix(cli): preserve existing skill on reinstall rename failure installSkillFromUrl() deleted the existing skill directory before renaming the staged replacement. If the rename failed (e.g. EPERM on Windows), both copies were lost. Use a rollback-capable swap: rename the old directory to a backup sibling first, rename staging in, then remove the backup. On rename failure, restore the backup. Fixes #10187 Co-authored-by: Qwen-Coder <[email protected]> * fix(cli): filter install artifacts from skill loading and add rollback test Skip `.backup-*` and `.installing-*` entries in both `loadSkillsFromDir` (skill-load.ts) and `SkillManager.loadSkillsFromDir` (skill-manager.ts) so stale transient directories from a crashed reinstall are never loaded as duplicate skills. Add a regression test that verifies the rollback path: when the staging→final rename fails (EPERM), the original skill content is restored and no backup/installing siblings remain. Fixes: review findings on PR #10189 Co-authored-by: Qwen-Coder <[email protected]> * fix(cli): use regex match for install artifact filter and verify reinstall content - Change substring match (.includes) to regex (/\.backup-\d/, /\.installing-\d/) so legitimate skill names like `db.backup-restore` are not incorrectly skipped - Add assertion in reinstall test to verify the new content was actually written Co-authored-by: Qwen-Coder <[email protected]> * fix(test): add getToolRegistry to telemetrySwap test mock config The restoreLoadedSkillsFromHistory method added to client.ts calls this.config.getToolRegistry() but the minimal config mock in client.telemetrySwap.test.ts did not provide it, causing 9 test failures with "TypeError: this.config.getToolRegistry is not a function". Co-authored-by: Qwen-Coder <[email protected]> * fix(cli): anchor skill install-artifact filter and pin it with tests The reinstall-artifact filter used unanchored regexes (/\.backup-\d/ and /\.installing-\d/), which matched anywhere in a directory name, so a legitimate skill dir like `db.backup-2024` installed fine but was silently never loaded. Anchor both patterns to the actual artifact shape (`<slug>.backup-<pid>-<timestamp>` / `<slug>.installing-<pid>-<timestamp>`) in both loaders (skill-load.ts and skill-manager.ts). Add skill-install-artifacts.test.ts pinning each loader's filter block: seeds a real skill plus `.backup-<pid>-<ts>` / `.installing-<pid>-<ts>` siblings holding valid SKILL.md files and a legit `db.backup-2024` dir, asserting only the real and legit dirs load. Verified red when either filter is removed or reverted to the unanchored form. Co-authored-by: Qwen-Coder <[email protected]> * fix(cli): reserve install-artifact suffix in skill name validation The anchored loader filters skip directories shaped exactly like the reinstall swap artifacts (`<slug>.backup-<pid>-<timestamp>` / `.installing-<pid>-<timestamp>`). A skill whose slug/name legitimately ends in that shape installed successfully yet was silently skipped by both loaders, so it never appeared in listings and could never be invoked while every reinstall kept reporting success. Reject such slugs in validateSkillSlug and such names in validateWorkspaceSkillName with a clear error, so the loader filters have no API-installable name left to eat. Names merely containing the markers (e.g. db.backup-restore) remain valid. Co-authored-by: Qwen-Coder <[email protected]> * test(core): pin $ anchor and literal dot of artifact filter regexes Seed two more legitimate skill dirs into the loader-filter tests: `db.backup-2024-1234-archive` (name continues after the two digit groups, so dropping the `$` end anchor filters it and both tests go red) and `data-backup-2024-06` (hyphen instead of a dot before `backup-`, so dropping the `\.` requirement filters it and both tests go red). Both mutations were confirmed to turn the suite red before restoring the production regexes. Co-authored-by: Qwen-Coder <[email protected]> * fix(cli): drop telemetrySwap test hunk already on main origin/main already carries the same getToolRegistry mock (plus later LlmClient rename), so merging both copies yields a duplicate object literal property (TS1117) that breaks the build. Align this file with main as suggested in the local verification report (E5). Co-authored-by: Qwen-Coder <[email protected]> * fix(cli): allow cleanup of legacy skill artifact names Keep reserved install-artifact suffix rejection on install paths while allowing management paths to delete or toggle legacy skills whose names already match that shape. Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: yiliang114 <[email protected]> |
||
|
|
1e695c34ff |
fix(ci): retain disk samples when npm install fails (#10517)
* ci: gate heavy jobs on a disk floor and persist pressure samples The in-repo slice of #10035: fail fast on a saturated self-hosted host before npm ci instead of dying on ENOSPC mid-run, and keep the disk-pressure timeline from a failed run as an artifact so the peak can be correlated with the job and runner after cleanup reclaims the host. * ci: single-quote the failure() condition to satisfy yamllint quoted-strings Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com> * ci: keep TMPDIR routing block byte-identical across test legs The sample-file setup landed inside the routing block that no-ak-integration-ci pins identical across test/test_macos/test_windows, breaking the identity assertion. Move the DISK_SAMPLES definition and DISKCONTEXT header ahead of the routing block; the DFSAMPLE lines still carry the routed tmpdir. * fix(ci): validate disk floor overrides * fix(ci): reject oversized disk floor overrides Keep the disk-floor shell comparisons inside bash's signed integer range by rejecting numeric overrides that cannot be compared safely. Add the existing helper test coverage for the overflow boundary.\n\nCo-authored-by: Qwen-Coder <[email protected]> * test(ci): pin the disk-floor overflow guard boundaries Pin the three behaviours of validate_floor_override that the current suite leaves unpinned, each verified to catch its mutant: - INT64_MAX is a legal floor and must reach the disk comparison (tightening the length check to -gt 18 now goes red). - Zero-padded overrides padded past 19 raw characters are normalized and accepted (removing the leading-zero strip now goes red). - 20-digit values are rejected by the length branch, which the existing 19-digit case never exercises (widening to -gt 20 now goes red). Co-authored-by: Qwen-Coder <[email protected]> * refactor(ci): simplify disk floor validation and sampling Co-authored-by: Qwen-Coder <[email protected]> * fix(ci): preserve the TMPDIR sampler sentinel Co-authored-by: Qwen-Coder <[email protected]> * fix(ci): retain disk samples from dependency install Co-authored-by: Qwen-Coder <[email protected]> * refactor(ci): reuse the workflow parser in disk tests Co-authored-by: Qwen-Coder <[email protected]> * chore(ci): trigger checks after retargeting to main Co-authored-by: Qwen-Coder <[email protected]> * test(ci): pin disk-pressure workflow contract Co-authored-by: Qwen-Coder <[email protected]> * fix(ci): harden install disk sampling Keep the install sampler non-fatal regardless of command-substitution order and satisfy the regex lint rule in its contract test. Co-authored-by: Qwen-Coder <[email protected]> * ci: record ci.yml growth in the size baseline The disk-floor and disk-sampling legs added ~1 KB of real workflow content; combined with prior main growth this pushed ci.yml 4848 bytes past its recorded 98043, over the 4096 allowance. Bump the ratchet to the current 102891 so the check reflects the reviewed growth. Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com> Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
cefcff78b2 |
fix(ci): gate no-AK installs on disk capacity (#10539)
* ci: gate heavy jobs on a disk floor and persist pressure samples The in-repo slice of #10035: fail fast on a saturated self-hosted host before npm ci instead of dying on ENOSPC mid-run, and keep the disk-pressure timeline from a failed run as an artifact so the peak can be correlated with the job and runner after cleanup reclaims the host. * ci: single-quote the failure() condition to satisfy yamllint quoted-strings Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com> * ci: keep TMPDIR routing block byte-identical across test legs The sample-file setup landed inside the routing block that no-ak-integration-ci pins identical across test/test_macos/test_windows, breaking the identity assertion. Move the DISK_SAMPLES definition and DISKCONTEXT header ahead of the routing block; the DFSAMPLE lines still carry the routed tmpdir. * fix(ci): validate disk floor overrides * fix(ci): reject oversized disk floor overrides Keep the disk-floor shell comparisons inside bash's signed integer range by rejecting numeric overrides that cannot be compared safely. Add the existing helper test coverage for the overflow boundary.\n\nCo-authored-by: Qwen-Coder <[email protected]> * test(ci): pin the disk-floor overflow guard boundaries Pin the three behaviours of validate_floor_override that the current suite leaves unpinned, each verified to catch its mutant: - INT64_MAX is a legal floor and must reach the disk comparison (tightening the length check to -gt 18 now goes red). - Zero-padded overrides padded past 19 raw characters are normalized and accepted (removing the leading-zero strip now goes red). - 20-digit values are rejected by the length branch, which the existing 19-digit case never exercises (widening to -gt 20 now goes red). Co-authored-by: Qwen-Coder <[email protected]> * refactor(ci): simplify disk floor validation and sampling Co-authored-by: Qwen-Coder <[email protected]> * fix(ci): preserve the TMPDIR sampler sentinel Co-authored-by: Qwen-Coder <[email protected]> * fix(ci): gate no-AK installs on disk capacity Co-authored-by: Qwen-Coder <[email protected]> * chore(ci): trigger checks after retargeting to main Co-authored-by: Qwen-Coder <[email protected]> * test(ci): pin no-AK disk gate contract Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com> Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
3da6d3b4e2 |
ci: allow full test jobs to finish on ECS runners (#10558)
* ci: allow full test job to finish on ECS runners Co-authored-by: Qwen-Coder <[email protected]> * ci: extend full test timeout for slow ECS runs Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
f772343c78 |
ci: update ECS runner fleet labels (#10629)
Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
2e1942537b |
fix(cli): shorten sandbox hostname for image-ID container names (#10624)
* fix(cli): shorten sandbox hostname for image-ID container names A regular container name derived from an image ID (sha256-<64hex> plus the -<8hex> suffix = 80 chars) exceeds Linux HOST_NAME_MAX (64), so `docker run` fails with `sethostname: invalid argument` (exit 125) whenever QWEN_SANDBOX_IMAGE is an image ID -- the documented shape after #10605. Keep the full name for --name and derive --hostname from a short hash when the container name is too long. Follow-up to F1 in the #10605 verification report. Co-authored-by: Qwen-Coder <[email protected]> * fix(cli): preserve valid sandbox hostnames Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
ed6fc69ae9 |
feat(core): remind the model of the active output style every turn (#10282)
* feat(core): remind the model of the active output style every turn Every non-default output style now sends its turn reminder as a <system-reminder> on each user and cron turn, next to the date, plan-mode and arena reminders. The headless-Learning gate moves into a shared resolveEffectiveOutputStyle() so the prompt and the reminder can never disagree about which style is active. Reminder text is escaped before it is wrapped, so a file-supplied turnReminder cannot close the block. Claude-Session: https://claude.ai/code/session_01Gk1gryVzWuW58zkBtoBvnM * fix(core): resolve the interaction mode only when a style is active Hand-written Config mocks that never set a style (client-goal.test.ts) should not need the interaction-mode accessors, and the default path should not pay for a mode lookup it does not use. Claude-Session: https://claude.ai/code/session_01Gk1gryVzWuW58zkBtoBvnM * fix(core): skip the style reminder when the prompt carries no style A custom systemPrompt routes through getCustomSystemPrompt() and a QWEN_SYSTEM_MD override replaces the base prompt verbatim — neither carries an output-style section, yet the per-turn reminder still fired for them. Gate the reminder on the same decision that puts the style section into the prompt: a shared isSystemMdActive() in prompts.ts, consulted by getCoreSystemPrompt() and the reminder site alike, plus the existing getSystemPrompt() override check. Also route the reminder envelope through the centralized wrapSystemReminder() instead of a third inline copy, and pin the intended cron-turn inclusion with a test. * fix(core): give write-file tests a unique per-run root dir A fixed /tmp/qwen-code-test-root breaks whenever a prior run by another user (e.g. a sandboxed root run on a shared runner) leaves the directory behind: mkdirSync(recursive) no-ops on the foreign directory and every write into it EACCESes. Reproduced the 80 deterministic-gate failures locally with a stale root-owned directory present; mkdtempSync isolates each run from any leftover state. * refactor(core): centralize the turn-reminder style decision in prompts.ts Co-authored-by: Qwen-Coder <[email protected]> * test(core): pin the main-session override replacement and model forwarding The prompts.test.ts matrix only asserted that a custom systemPrompt override appears in the rendered prompt, never that it replaces the default base — and the config.getModel() forwarding in getMainSessionBaseSystemPrompt had no witness after the client-side assertions moved to the prompt layer. Both mutants (layering the override on the base; dropping the model forward) shipped green in a scratch probe. Add the replacement assertion and an end-to-end coder-model case; each now kills its mutant. Co-authored-by: Qwen-Coder <[email protected]> * refactor(core): keep getMainSessionBaseSystemPrompt in client.ts The output-style work had moved `getMainSessionBaseSystemPrompt` and its `MainSessionPromptConfig` type from client.ts to prompts.ts. The move was incidental to this PR — it widened the diff across two modules and rewrote every `getCoreSystemPrompt` mock in client.test.ts into a `getMainSessionBaseSystemPrompt` one, none of which the feature needs. The function is back where main keeps it, carrying the one change the feature does require: it resolves the style through `resolveMainSessionOutputStyle(config)` rather than `config.getOutputStyle()`, so the prompt and the per-turn reminder cannot disagree about which style is in force. `resolveMainSessionOutputStyle` and `isSystemMdActive` stay in prompts.ts, where they are new rather than moved. client.test.ts is main's file plus the reminder tests; prompts.test.ts pins the prompt against the resolver by importing the builder from client.js. Claude-Session: https://claude.ai/code/session_018dYE4LwSMeMPFchXk5UBdM * test(core): stub QWEN_CODE_TOOL_CALL_STYLE in the main-session prompt suite The 'forwards the config model to the base prompt' pin asserts on model-detected tool call examples, but getToolCallExamples consults QWEN_CODE_TOOL_CALL_STYLE before model detection. Without the stub, an exported value makes the pin vacuous (qwen-coder) or spuriously red (general). * fix(acp): send the output-style turn reminder on ACP prompts too The ACP session assembles its own per-turn reminders because it calls the chat directly and bypasses LlmClient.sendMessageStream; the style reminder was missing from that list, so an ACP prompt carried the style section but never the nudge. It now reads the same resolveMainSessionOutputStyle() decision as the prompt, so a custom system prompt or QWEN_SYSTEM_MD keeps it silent there as well. Also pins that Retry, Notification and Teammate turns carry no style reminder. Claude-Session: https://claude.ai/code/session_01Gk1gryVzWuW58zkBtoBvnM * fix(core): treat an empty turnReminder as the generic reminder (#10282) * test(ci): budget ecs-pool load spikes in three flaky cli suites (#10282) Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: qwen-code-dev-bot <[email protected]> |
||
|
|
2268c9bc06 |
fix(sdk): make daemon bundle size advisory (#10630)
Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
12343f2598 |
feat(config): let operators size or disable the Goal token budget (#10543)
* feat(config): let operators size or disable the Goal token budget #9891 gave createGoalRuntime a tokenBudgetGrant option, but the only production constructor never passed it, so every session ran on the 30M default with no way to raise, lower, or switch it off. The number was already user-visible (get_goal reports it, a budget stop prints it); it just was not user-settable. `model.goalTokenBudget` sits beside the other run ceilings. A positive integer is the grant, in the tokens the Goal meter counts. 0 runs Goals with no budget by mapping to the runtime's non-finite opt-out, so the created Goal simply carries no tokenBudget field and nothing non-finite is persisted. Anything invalid falls back to the default with a debug warning. It reaches core Config the way maxSessionTurns does and is passed at the single createGoalRuntime call; the runtime is untouched. Verified live: with `{"model":{"goalTokenBudget":400000}}` in the workspace settings, the session journal's create record carries tokenBudget=400000 and the Goal completed in one turn at 166,503. Mutation probes: dropping the pass-through fails the 1234 and the 0 tests; dropping the 0-to-unbounded mapping fails the 0 test and the normalizer test. * fix(config): align goalTokenBudget fallback docs and accept -1 opt-out (#10543) Co-authored-by: Qwen-Coder <[email protected]> * test(cli): deflake serve bridge wiring tests under full-suite contention (#10543) Co-authored-by: Qwen-Coder <[email protected]> * fix(config): cap goalTokenBudget at 10x the default to catch typo zeros The setting's population is exactly "people typing zeros into a safety bound", and an extra zero on the 30,000,000 default was accepted silently, widening the runaway-spend window tenfold with no warning. Positive values above GOAL_TOKEN_BUDGET_CAP (300,000,000) now fall back to the default through the same invalid-value path as negatives, with the debug-log warning. An operator who genuinely wants more autonomy than 300M tokens per window has the explicit opt-out (0/-1); the cap is a typo guard, not a policy on long runs. Mutation probe: removing the upper bound fails exactly the new cap-boundary test (595 others green). * fix(cli): validate Goal token budget settings * chore: preserve main serve test formatting --------- Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: qqqys <[email protected]> |
||
|
|
954f3a92ce |
fix(core): keep no-follow reads protected where O_NOFOLLOW is missing (#10007)
* fix(core): keep no-follow reads protected where O_NOFOLLOW is missing O_NOFOLLOW does not exist on Windows: fs.constants.O_NOFOLLOW is undefined, so the `(O_RDONLY | (O_NOFOLLOW ?? 0))` flag expressions silently collapse into a plain open that follows symlinks, dropping the symlink/TOCTOU hardening added for @-referenced file reads (#7206). Add a cross-platform open helper that uses the kernel flag where present and otherwise compensates with an lstat -> open -> fstat identity check, refusing symlinked paths, identity races, and zero-inode filesystems (fail-closed, matching #8290/#9857). Route the confirmed no-follow read call sites through it: validated @-file reads, session metadata reads, background-shell output tails, untracked diff line counts, the workspace registration store, and session-artifact workspace status. Co-authored-by: Qwen-Coder <[email protected]> * fix(core): keep the no-follow helper mockable by fs spy suites openSyncNoFollow bound node:fs through a namespace import, which vitest resolves to its own copy of the externalized CJS module. Suites that spy the fs object — sessionService.rename.test.ts stubs openSync/readSync for fabricated session paths — never intercept that copy, so the open threw on the mocked paths and the catch-all reported "no title" (10 of 19 tests red, the CI Test failure). Take the fs binding through the default import the way the callers' suites spy it, teach the doMock factories to carry the stub on the default binding too, and stub lstatSync/fstatSync in the rename suite so the Windows lstat -> open -> fstat fallback accepts the fabricated paths as well. Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com> * fix(core): distinguish inode-unverifiable refusals from ELOOP The fallback's inode-0 fail-closed refusal carried code 'ELOOP' like a genuine symlink refusal, so consumers with ELOOP-specific handling misfired on LEGITIMATE files on inode-0 volumes (Windows FAT/exFAT/SMB, where Node reports ino 0): session-artifact workspace status flagged a contained file as an escape, the workspace registration store reported a regular store as "must be a regular file", and untracked text files rendered as binary with dropped hunks. Give the inode-unverifiable refusal its own code (EUNVERIFIABLE) plus an isUnverifiableIdentityError guard, keep ELOOP for genuine symlink refusals and identity races, and adjust the three consumers: the artifact status degrades to plain 'missing', the store surfaces an identity-unverifiable error, and the untracked diff read falls back to the pre-#8227 plain read (its lstat gate already rejected symlinks and non-regular files). Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com> * refactor(core): drop the unused flags/mode params from the no-follow opens No call site and no test ever passes flags or mode — repo-wide, every invocation opens by path alone — yet the fs.open-shaped signature was exported through the core index and invited write/create flags through a helper whose docs, lstat -> open -> fstat semantics, and tests cover only the read-only case (O_WRONLY | O_CREAT would create on POSIX while the Windows fallback's pre-open lstat throws ENOENT for the same input). Hardcode the read-only base flags and delete resolveBaseFlags; a PR that first needs more can re-add them with a caller and tests. Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com> * test(core): pin the symlink refusal for the plural session-field read readLastJsonStringFieldsSync was rerouted through the no-follow helper in the same pass as the single-field variant, but only the latter got a symlink-refusal test — reverting the plural open to a plain fs.openSync kept the whole suite green. Mirror the Windows-flag-set symlinked-session test for the plural variant, asserting the all-undefined empty result; the test is red on that mutant (leaked-secret surfaces) and green on the restored code. Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com> * test(core): pin the async identity re-check of the no-follow fallback The async fallback's TOCTOU identity re-check in openNoFollow (assertSameIdentity plus close-on-rejection) was pinned by no test: deleting the whole try/catch block kept the suite green, because the async symlink tests reject at the earlier isSymbolicLink() check and only the sync variant's re-check was driven. A refactor dropping that block would ship green while a path swapped for a symlink between lstat and open gets read through on Windows and the rejection-path handle leaks unclosed. Add the async identity-change test using the same prototype trick as the sync one, applied to fs.promises.lstat (the real opened FileHandle's stat() cannot be intercepted through fs mocks): the doctored before-stats carry ino + 1, so the real handle's stat mismatches and the open must reject with code ELOOP after closing the handle (asserted through a close spy). The test is red on the mutant (block deleted: 1 failed | 10 passed) and green on restored code (11 passed). Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com> * fix(acp): keep no-follow helper off the core barrel Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com> Co-authored-by: Qwen-Coder <[email protected]> * fix(cli): keep registration read off core barrel Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com> * test: resolve no-follow core subpath in consumers Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com> Co-authored-by: Qwen-Coder <[email protected]> * test(core): exercise output-tail no-follow fallback Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com> * test(cli): pin unverifiable registration identity Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com> * build: map noFollowOpen subpath in typecheck programs Both integration-tests and acp-bridge import the new @qwen-code/qwen-code-core/noFollowOpen subpath but had no paths entry for it, so typecheck resolved it through core's pre-built dist and failed with TS2307 on dist-less trees. Add the source entries next to the sibling subpaths, following the documented 'keep in sync with the exports maps' invariant. Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com> * test(core): pin fd close, dev check, and pre-open snapshot Three pins for the no-follow fallback's guarantee surface: - spy fs.closeSync in both sync rejection tests (identity change, inode 0) so a dropped close in openSyncNoFollow's catch turns red, mirroring the async closeSpy pin - add a dev-mismatch identity variant so deleting the dev comparison from assertSameIdentity turns red (ino alone is unique per device) - add sync+async tests that perturb every lstat after the first call and expect the open to SUCCEED, pinning that the identity re-check compares against the pre-open lstat snapshot rather than a fresh post-open lstat Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com> * test(core): pin inode-0 degradation branches in sessionArtifacts and gitDiff Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com> * test(core): run sync fallback identity re-checks on all platforms The ino-mismatch and dev-mismatch identity-change tests never plant a symlink: they write a plain file and perturb fstatSync via vi.doMock. The itNoSymlink guard therefore only skipped them on win32, the one platform where the lstat/open/fstat fallback is the production path. Switch both to plain it, matching the async twin. Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com> * build(cli): map noFollowOpen subpath in cli typecheck paths packages/cli imports @qwen-code/qwen-code-core/noFollowOpen in workspace-registration-store.ts but had no paths entry for the leaf, so cli typecheck resolved it through core's compiled dist and breaks in deep-cleaned worktrees. Mirror the entry already added for acp-bridge and integration-tests. Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com> * test(core): dedupe no-follow-open fallback mocks, pin best-effort close Extract the repeated O_NOFOLLOW-less node:fs mock skeleton into a shared mockNoFollowFs factory (the load-bearing `default` member stays) plus a perturbedStats helper, and drop the six per-test doUnmock/resetModules blocks already covered by the describe-level afterEach. Add sync, async, and inode-0 variants asserting a failing rejection-path close never masks the pinned ELOOP / EUNVERIFIABLE refusal codes. Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com> Co-authored-by: Shaojin Wen <[email protected]> Co-authored-by: root <[email protected]> |
||
|
|
569756ee50 |
feat: manage workspace-scoped extension skill state (#10600)
* docs: design workspace-scoped extension resource state * feat(extensions): add workspace-scoped skill state * test(serve): align extension state capability expectations |
||
|
|
8fcf9e76f9 |
feat(serve): establish workspace runtime ownership (#10593)
* feat(serve): establish workspace runtime ownership * fix(serve): harden workspace runtime lifecycle --------- Co-authored-by: 钉萁 <[email protected]> |
||
|
|
d6878a84e8 |
fix(ci): recover /resolve requests lost to moved heads, fork pushes, 503s and drafts (#10429)
* fix(ci): recover /resolve requests lost to moved heads, fork pushes, 503s and drafts
Of the 294 resolutions the /resolve agent has ever produced, 17 were thrown
away because the PR head moved while it worked, ten more agent runs were
spent on fork PRs whose push was then rejected for lack of maintainer
edits, a 503 from the permission API denied writers silently, and 182
explicit requests on drafts were answered with a skip comment.
- Report result: when the lease push is declined for a moved head, fetch
the new head, redo the base merge on it, take the agent's resolution for
every still-conflicting file the new commits did not touch, re-run the
marker / clean-merge / base-changed-files guards, and push with a lease
on the new head. Anything else gives up and reports "moved" as before.
The agent is never re-run.
- Prepare: refuse fork PRs with "Allow edits by maintainers" off before
the agent runs, via maintainerCanModify.
- authorize: retry a 5xx from the permission API up to three times before
treating it as a denial; still fails closed.
- Prepare: drop the draft gate for the command (the review lane keeps its
own).
The replay functions are exercised against a real git fixture in three
cases; contract pins cover the push wiring, the fork preflight, the draft
gate and the retry loop.
* ci: record the resolve-pr replay growth in .size-baseline
The moved-head replay, the fork preflight and the authorize retry grow
qwen-code-pr-review.yml past the 4096-byte ratchet; the growth is the
recovery logic itself, so bump the baseline.
* fix(ci): pass BASE_REF into the /resolve Report result step
The moved-head replay merges the base branch again, but the step's env
never defined BASE_REF, so under `set -u` the first replay would have
aborted the step. Add the env entry and a test that walks every run
block of resolve-pr and fails on any expanded uppercase variable that
neither the job env, the step env, a shell assignment nor a runner
builtin defines — the fixture test injected BASE_REF itself, which is
why nothing caught it.
* fix(ci): fetch the moved head by URL in the /resolve replay
The post-agent scrub landing in #10428 removes the workspace .git/config
— and with it the `origin` remote — before any credentialed step runs,
so the replay's `git fetch origin` would fail there. Fetch by URL, with
an env hook (empty in production) that the fixture test points at its
bare repository; a pin forbids `git fetch origin` in the replay.
* fix(ci): classify moved heads before permission patterns in /resolve replay
Address the second-round review findings on the /resolve recovery paths:
- classify_push_failure now tests the lease-decline signature (stale
info, force-with-lease, non-fast-forward, fetch first) BEFORE the
permission patterns: git echoes the destination branch into the push
log, so a moved head on a branch named e.g. fix/permission-prompt or
fix-403-error matched the permission arm first and the replay never
ran — the exact loss class this PR recovers.
- The replay handles resolutions that deleted the conflicted file
(delete/modify conflicts): checkout fails on a path absent from the
resolved commit, so take the deletion via git rm instead.
- The replay-success comment no longer claims files were taken from the
agent's merge when the replay merged clean, and says that the run
artifact describes the original resolution, not the pushed tree.
- Tests: behavioural coverage for the authorize retry loop against a
scripted gh (3 attempts, 5s/10s backoff, fail-closed denial arms) and
for the classifier against real push logs (branch-name echoes,
re-classification after a failed replay push); fixture coverage for
the empty-merge give-up and the deletion resolution; the draft-gate
pin now names the real delay-automatic-review job without the silent
whole-file fallback.
* fix(ci): give the /resolve replay its own committer identity, match git's parenthesised lease reasons
The replay commits with `git commit -C`, which reuses the agent's author
and message but takes the committer from the environment or config. The
only identity in production was the workspace .git/config written by
'Prepare pull request branch' — the file the post-agent scrub (#10428)
removes before this step. Export the bot identity inside the replay;
the fixture now runs it with no identity in the environment and no
.git/config, and asserts the committer.
Also match the moved-head push failure on git's parenthesised reason —
`(stale info)`, `(fetch first)`, `(non-fast-forward)` — rather than the
bare words, which are legal branch-name substrings echoed into the same
rejection line.
* fix(ci): keep the /resolve replay's pathspecs literal and lease reasons anchored
The replay's per-file git commands treated the literal conflicted filename
as a pathspec pattern, so a file named `spec[1].md` widened diff/checkout/rm
to its sibling `spec1.md` — give-ups on untouched files, and `git rm` staging
a sibling's deletion. Export GIT_LITERAL_PATHSPECS inside replay_on_moved_head
and pin both shapes with glob-named fixtures.
Anchor the classifier's moved arm to end-of-line: git prints the reason last,
so a branch merely containing `(non-fast-forward)` no longer matches before
the permission arm; pinned by branch-echo classifyLog cases that also guard
the parenthesised patterns against a bare-word regression.
Fold the four copies of the workflow-YAML block extractor into one
extractBlock helper and the duplicated classify scaffolding into one shared
spawn runner. Reword the replay comment so a clean-merge replay no longer
implies files were taken from the agent's merge. Re-record the size baseline
at the merged workflow size.
* fix(ci): preserve the pushed tree when the /resolve replay lands
The `qwen-resolve-pr-N` artifact is uploaded before 'Report result'
runs, so after a successful moved-head replay it describes the original
resolution, not the tree that was pushed. Record the replay's diff
against the base and its SHA under WORKDIR/pushed/ and upload them as
`qwen-resolve-pr-N-pushed` in a step after 'Report result' (same
always()/decision gate as the first upload, so it still runs when the
report exits 1); the replay-success comment now points at that
artifact. A contract test pins the regeneration, the second upload's
name/path/gate and its position after 'Report result'.
* fix(ci): re-record the review-workflow size after the pushed-tree step (#10429)
* test(ci): point the /resolve replay tests at the publish job after the split re-home
The conflict resolution in
|
||
|
|
dd5b857c8e |
feat(web-shell): allow copying terminal turn errors (#10001)
* feat(web-shell): allow copying terminal turn errors * fix(web-shell): expose turn error copy on touch devices * test(web-shell): cover turn error action visibility --------- Co-authored-by: 恪文 <[email protected]> |
||
|
|
bc186116c6 |
fix(ci): align non-Linux Vitest unhandled-error handling across unit suites (#10443)
* fix(ci): extend the vitest RPC-timeout exemption to every unit suite (#10438) * refactor(ci): collapse duplicated vitest exemption rationale to witness pointer (#10438) * refactor(ci): extend vitest rationale pointer consolidation to remaining unit configs (#10438) |
||
|
|
6608cd3b3d |
fix(ci): coordinate Docker sandbox workloads on shared hosts (#10605)
Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
8ca93b4902 |
feat(channels): Enable concurrent named task control (#10574)
* feat(channels): enable concurrent named task control Co-authored-by: Qwen-Coder <[email protected]> * codex: address PR review feedback (#10574) Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
be5f00eb80 |
test: harden release timing-sensitive waits (#10615)
Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
4123aa6fda |
fix(ci): run /resolve without the container sandbox and pin its CLI version (#10428)
* fix(ci): run /resolve without the container sandbox and pin its CLI version Every /resolve since 2026-08-15 has failed before the agent looked at the conflict: 0 of 81 agent runs pushed a resolution, against 84% of the 273 runs before that date. Three causes, none of them the model: - 14 runs: `@qwen-code/qwen-code@latest` pointed at an unresolvable 0.21.12 and the action's npm install died on `notarget`. - 24 runs: #9252 made the settings block take effect for the first time, `"sandbox": true` with it; the CLI hopped into the versioned ghcr image, which lags the npm release, and died on `manifest unknown` (#9898). - 40 runs: with the image present the sandboxed agent produced no output until the 120-minute job timeout; the merge had not even started. No sandboxed /resolve run has ever pushed a resolution and every pushed one ran outside the container, so pin tools.sandbox to false, pin the CLI to 0.21.10 (the last version with a measured record on this job), give the agent step a 75-minute timeout (p99 is 70), and when the step ends without outcome=success report it as an infrastructure failure that a re-run will repeat, instead of the generic "did not complete successfully" that read as the model giving up and drew 13 days of re-runs. The two contract tests that pinned sandbox: true now pin false; new tests pin the exact-semver version input, the step timeout mirrored into the comment, and the infra-failure wording. * ci: record the resolve-pr growth in .size-baseline and drop two stale sandbox mentions The settings-block rationale for tools.sandbox=false and the version pin grow qwen-code-pr-review.yml by ~7.5 KB, past the 4096-byte ratchet; the growth is the incident record itself, so bump the baseline. Two comments on the scope guard still named the sandbox as part of the containment story; align them with the settings block (write authorization + no agent token + ephemeral runner). * fix(ci): scrub agent-planted runner state before the resolve-pr credentialed push The no-sandbox /resolve agent runs --yolo before 'Resolution check' and 'Report result' in the same job: anything it appends to $GITHUB_ENV reaches every later step, and it writes the workspace. A prompt-injected agent could therefore plant BASH_ENV/LD_* hijacks, PATH shims, git-config url rewrites or a pre-push hook that the workflow-scope push token then walks through (measured by review probes; the pre-fix shape fires all three channels). Harden both post-agent steps: pin BASH_ENV/LD_* empty at the step boundary (they execute before any body line runs), pin PATH, drop git's env channels, ignore host/global config, remove the agent-writable local .git/config, and run the push hookless (--no-verify, core.hooksPath=/dev/null). Pin the follow-up bot's CLI version too — it was the one sibling still following npm `latest` through the 2026-08-15 notarget incident class. Tests: witness assertions for every hardening layer (each one red when its layer is removed), arm-slice pins for the infra/generic report bodies and the failure_kind=infra write, and the step-vs-job timeout pair now reads the job ceiling instead of duplicating it. Bump the size baseline for the deliberate workflow growth. * fix(ci): invoke the /resolve agent directly with decoyed runner files, on top of the post-agent scrub Complements the scrub that 'Resolution check' and 'Report result' now open with (PATH pinned, git env channels dropped, every config scope ignored, .git/config removed, BASH_ENV/LD_* pinned empty): those close what the later steps can see, but a $GITHUB_ENV append made by the agent is applied by the runner to every later step's environment, where an unset list can only chase known names. So the agent is now invoked directly — the pinned CLI installed in its own step — with its invocation's $GITHUB_ENV, $GITHUB_PATH, $GITHUB_OUTPUT and $GITHUB_STEP_SUMMARY pointed at decoys that die with the step, inside a ::stop-commands:: window with a random resume token, the way the review lane runs its agent. Settings go to a per-run QWEN_HOME instead of the action input. On the scrubbed side: 'Show run artifacts' gets the same scrub, every post-agent git call goes through a wrapper that disables hooks, the fsmonitor command and credential helpers, and 'Report result' gives gh a fresh empty config dir instead of leaving ~/.config/gh in reach. The settings-block comment records the residual: a root-capable agent on a hosted runner can defeat any in-job measure; the structural answer is a downstream job for the credentialed push, tracked separately. Tests pin the direct invocation, the decoys, the stop-commands window, the scrub and wrapper on every post-agent step, the BASH_ENV/LD_* pins, the gh config dir and --no-verify; the size baseline moves with the growth. * chore: drop an unrelated lint-staged reformat picked up by the merge commit * fix(ci): close the resolve-pr containment bypasses from review round 4 (#10428) Truncate the real runner command files after the agent (decoys only mask the invocation env), wrap the invocation in GNU timeout so the resume/truncation survive a hung agent, remove a fork-PR-committed workspace settings file that outranks the per-run settings, refuse a gitfile-replaced .git before the checks and the credentialed push, give 'Show run artifacts' the full scrub with step-env pins, pin absolute shells on every post-agent step, drop the dead agent env keys, and pin each of these plus the exit-code propagation, scrub ordering, and both version pins in the contract tests. * fix(ci): publish the /resolve resolution from a job that never ran the agent Review rounds 2-5 on this PR each demonstrated another entrance into the credentialed push from the same job the unsandboxed agent ran in: git config scopes and hooks, a gitfile-replaced .git, a moved ref, PATH shims, appends to the real $GITHUB_ENV (discoverable on disk despite the decoys), lowercase proxy variables, GIT_TRACE, a FIFO in place of a command file, BASH_FUNC_* exports, and finally a detached process reading /proc/<pid>/environ of the push step — which no in-job scrub can reach. A denylist over that surface cannot be closed, so the push moves. resolve-pr now ends without credentials once the agent has run: a new 'Package resolution' step bundles the resolution (objects and one named ref, prerequisite = the head the agent resolved from) into the run artifact alongside the report files, and the job exposes decision, pr_number, the agent step's outcome and the prepare-time refs as outputs. The scrubs, the .git guards and the BASH_ENV/LD_* pins that tried to harden the old post-agent steps are gone with them. publish-resolution (new, needs resolve-pr, always() && decision == run) checks out fresh, downloads the artifact, fetches the base and PR head refs from GitHub, pins HEAD_FETCH_REF to the head the agent resolved from, `git bundle verify`s the bundle against that, imports it and requires the resolution to descend from that head — then runs the unchanged 'Resolution check' guards and 'Report result' push/comment. A missing artifact (agent job cancelled or crashed) is reported as an infrastructure failure. Both jobs hold the qwen-pr-head-write lock. Tests: the contract pins move to the publish job and now assert the structure itself — no secret in resolve-pr after the agent step, no agent in the publish job, the token-bearing step only there, matching artifact names, the verify/import/lineage order — and a fixture test drives the real 'Package resolution' and 'Resolution check' run blocks across a bare origin, an agent-side clone and a fresh publish-side clone: happy path, a bundle that does not descend from the head, a bundle with an unknown prerequisite, a no-op against a clean base and its refusal against a conflicting one, a missing artifact, and an out-of-scope file smuggled in the bundle. Dropping the lineage check, the bundle verify, the head pinning or the missing-artifact branch each reds it. * test(ci): pin the /resolve agent invocation wiring and ordering Round-5 suggestions: --prompt and --auth-type are asserted on the invocation; `set +e` must sit between the stop token and the invocation and `set -e` resume after the command-file truncation; the workspace .qwen/settings.json removal must precede the qwen invocation. * fix(ci): keep /resolve re-runs from republishing a stale resolution Round-6 review findings: R6-2 — the run artifact name carried no run_attempt suffix on either side, so a "Re-run failed jobs" failed the second upload on the first attempt's same-name artifact and the publish job's by-name download picked up attempt 1's bundle — re-publishing the stale resolution as if it were the re-run's work. Suffix both sides (and the user-facing comment references) with the attempt, mirroring the sibling review lane's documented doctrine for the same upload-artifact version, and extend the artifact-name agreement pins in both contract suites. R6-1 — the two `if: "${{ always() }}"` lines added by the structural split are bare double-quoted strings, which the enforced yamllint quoted-strings rule rejects, failing the Test job. Flip them to single quotes, the form eight sibling workflows already use. * fix(ci): let /resolve partial re-runs publish the agent's resolution Round-7 review findings: R6-2 (fix-induced) — the run_attempt artifact-name suffix that fixed the stale-republish defect broke "Re-run failed jobs" the other way: the re-run executes only publish-resolution, whose attempt number has no artifact, so the by-name download missed the agent's upload, 'Resolution check' called a SUCCESSFUL agent an infrastructure failure, and 'Report result' posted a comment self-contradictory on every clause ("outcome=success ... CLI install, model endpoint or infrastructure failure ... Requesting /resolve again will fail the same way") while the resolution stayed unpublished — every further partial re-run repeated it. Thread the attempt that ran the agent across the job boundary (the runner provides GITHUB_RUN_ATTEMPT; the agent never writes it) and download by it, so the partial re-run picks up the artifact that actually exists and publishes it. For the residual case — the agent succeeded but the artifact is genuinely gone, upload failure or expiry — classify the lost artifact (failure_kind=artifact_missing) instead of the agent run, and word the comment accordingly: the agent finished, the result could not be published, and "Re-run all jobs" is the one recovery that re-produces the artifact. Fixture scenario (d) now drives the real 'Resolution check' block across both arms and reds on the pre-fix shape. R7-1 — the follow-up bot's qwen_cli_version pin is inert on its primary lane: the pinned action's 'Install Qwen Code' step skips when qwen is already on PATH (verified at the pinned SHA), always true on the ecs-qwen pool whose fleet CLI update-ecs-runner-qwen.yml maintains, so the comment's "the pin wins" falsely certified there. Rewrite it to state the real contract — fleet version authoritative on ecs-qwen, the pin governing the ubuntu-latest fallback lane — and say so in the contract test too. Also lands the round's mechanical items: .size-baseline records both workflows' exact new sizes (the ratchet passed only on its 4096-byte allowance); the unpinned gitfile guard in 'Package resolution' and GIT_TERMINAL_PROMPT=0 in 'Report result' get contract pins (mutation survivors M9/M10 of the sandboxed verification); a stale test cross-reference ('hardens the post-agent steps' no longer exists) and a garbled scope-guard comment repaired in passing. * fix(ci): bind the follow-up bot's CLI pin on its primary lane (#10428) R7-1 (review rounds 7-10): the follow-up bot's qwen_cli_version pin was a dead switch on its primary lane. The job routes to the ecs-qwen pool by default, where `qwen` is always on PATH — the fleet-installed CLI that update-ecs-runner-qwen.yml maintains at `latest` — and the pinned action's 'Install Qwen Code' step skips unconditionally when `qwen` is already present (verified at the pinned action SHA). The bot therefore ran the fleet CLI, not 0.21.10: a broken npm publish propagating through the fleet update would run the agent despite the declared pin — the exact 2026-08-15 notarget outage class the pin exists to prevent. The previous round rewrote the comment to concede the dead pin; this makes the pin effective instead. Add a job-local install step before the action: `npm install --prefix` into $RUNNER_TEMP (not -g — a global install on the shared ECS machines would fight the fleet-maintenance contract) and prepend its bin dir to GITHUB_PATH. The pinned copy outranks the fleet CLI on ecs-qwen and is the only CLI on the ubuntu-latest fallback lane; the action's install then skips over the pinned copy on BOTH lanes. The action's qwen_cli_version input now carries the installed version from the step output, so the declared input can never drift from what runs (same ensure->wire shape as qwen-triage.yml). The contract test pins the whole mechanism: step presence, exact-version pin, job-local --prefix install, registry pin, PATH prepend, ordering before the action step, and the input wired from the step output. Mutation probes: step deletion, -g install, dropped PATH prepend, dist-tag version, reverted input literal, and reordered steps all red. Also records the exact sizes in .size-baseline: qwen-code-pr-review.yml 193208 (one byte merged in from #10537's runner rename) and the follow-up bot's grown file. * fix(ci): give the /resolve publisher a per-run group so a completed resolution is never dropped The two-job split put publish-resolution in the shared qwen-pr-head-write-<pr> concurrency group. GitHub keeps only one PENDING job per group and replaces it when another same-group job is queued — cancel-in-progress:false protects only a RUNNING job. So after resolve-pr finished and uploaded a completed resolution, its publisher sat pending in that group, where a second /resolve, a later publisher, or an autofix writer could silently cancel it before the push or the result comment: a resolution that already succeeded, dropped with no response to the user — the exact silent-failure class this PR set exists to remove. (Reported by a reviewer on #10428.) Move the publisher to a per-run group (qwen-pr-publish-<run_id>): one member for its whole life, so it can never be the replaced pending job. Two publishers that do run at once stay correct through the push, not the group — it is already force-with-lease pinned to the head the agent resolved from, so exactly one wins and the other reports "moved" cleanly. The expensive, wasteful-to-race work — the agent — stays in the shared head-write group on resolve-pr, so #7355 (resolve vs autofix agent runs) stays covered. Tests: a new 'cannot drop a completed resolution while it waits to publish' pins the per-run publisher group, the head-write agent group and the lease; the triage and publish-job contracts are updated to the new split. Reverting the publisher to the shared group reds all three. --------- Co-authored-by: qwen-code-dev-bot <[email protected]> |
||
|
|
88a136317a |
fix(ci): scope release runner context to steps (#10598)
Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
c4803303fe |
fix(sdk): bump the browser daemon bundle budget to 216KB (#10602)
The daemon JSON-RPC error-detail surfacing grew the browser-safe daemon bundle from 215,998 to 220,220 bytes, past the 215KB gate, so every install on main fails its build script and CI is red across all PRs. Bump the budget by 1KB to cover the measured size with minimal headroom, following the file's bump-line convention. |
||
|
|
ec2fc5dd23 |
feat(cli): OpenTUI migration foundation batch — theme, a11y, clipboard, keys, dialogs scaffolding (#10146)
* feat(cli): OpenTUI foundation modules — theme, a11y, clipboard, keys, dialogs scaffolding Foundation batch of the OpenTUI migration tracked in #8662. Adds the renderer-neutral foundation modules under ui/opentui: theme family, a11y (plain-text, screen-reader), clipboard, key-map, mouse hit/caret, link-click + osc8 parity, early-input, exit guard/lifecycle, kitty negotiation, event-adapter, item-projection, slash dispatch (+ command parsing), commands context/output, help content, input history, and the dialog scaffolding primitives (core/shared) with the theme dialog. Two helpers land inside ui/opentui rather than utils/ to respect the utils leaf-layer rule (#9737). Stacked on the infra batch: consumes ui/model streaming model and @opentui deps. No reachable ink code changes beyond a one-line export addition in the shared osc8 module. * fix(cli): import originals instead of forking slash parser and dialog scope utils * fix(cli): address R1 review findings in OpenTUI foundation modules * fix(cli): align OpenTUI command host with the memory-file-count rename Upstream renamed setGeminiMdFileCount to setMemoryFileCount in the command UI contract; the rebase onto main surfaced the mismatch at build. Rename the host interface member, the bridge wiring, the dispatch stub, and the test mock to match. * fix(cli): address R2 review findings in OpenTUI foundation modules Round-2 review fixes (17 Critical + 10 Suggestion resolved in code): - dialogs-shared: move number-select flush out of the setState updater (StrictMode double-fires onSelect); split setActiveIndex (ink SET_ACTIVE_INDEX, lands on any in-range row) from highlightIndex (arrow keys skip disabled rows) so wheel navigation never sticks - event-adapter: chat_compressed notice mirrors ink formatCount ('~' prefix for estimated counts); vision_bridge_notice renders summary\nnotice; explicit projections for task_execution / findings_list / terminal_image keep multi-MB payloads off the transcript; retry-countdown-clear forwards isContinuation - slash-dispatch: isSlashCommandInput drops the '?' branch (ink gate routes ? input to the model); executeSlashCommand races the action against the abort signal; dialog effects carry the OpenDialogActionReturn payload; projected added-item text surfaces alongside non-handled effects (notice); message-shaped items project to their text; ui.history comes from env; absent sessionStats stamp now, not epoch; telemetry parity (recordSkillInvocation / recordAutoSkillCommandUsage / makeSlashCommandEvent) - item-projection: model stats render per-(model,source) sections with N/A for unpriced entries; Tool Calls line uses ASCII x like ink; redactProxy deduplicated via systemInfoFields export - theme: palette/syntax colors resolve through color-utils toHex before parseColor (ink CSS names / *bright names no longer degrade to magenta); unresolvable values stay unset - key-map: kitty 'kpenter' normalizes to 'return'; resolveCommands exposes ink's key fan-out (Ctrl+C fires QUIT + CLEAR_INPUT) - a11y: hardWrap delegates to wrap-ansi (word-boundary parity with ink's screen-reader path); markdown reducer tracks fence length, keeps fence-like lines literal inside fences and inner backticks in multi-backtick spans; stripAnsi delegates to strip-ansi plus a private-parameter CSI pass (SGR mouse, DEC save/restore) - clipboard: OSC 52 write gated on a TTY (stderr preferred), tests spy the stream instead of writing real sequences to the runner's terminal - exit-guard: independent per-key arm windows like ink - dialogs-theme: diff preview pane receives syntaxStyle/filetype * fix(cli): harden kitty probe and screen-reader writer per maintainer review - kitty-negotiation: KITTY_REPLY_RE requires at least one flag digit (\d+), so an echoed bare query \x1b[?u in PTY/CI environments no longer resolves true and locks the renderer into kitty mode on a terminal that never answers queries; the accumulation buffer keeps only a 256-byte tail (bounded memory, bounded rescan under byte floods); the settle-window drain is removed — an EventEmitter data listener cannot consume chunks from other listeners, so late replies flow to the renderer's input parser like any other terminal noise - a11y-screen-reader: ScreenReaderOutputWriter sanitizes written content (stripAnsi + drop bare C0/C1 controls, keep newlines) so the plain-text-only contract is enforced at the writer instead of trusting every future caller — smuggled OSC 52 clipboard writes or title/cursor sequences cannot execute on the main screen * fix(cli): address ytahdn independent review findings in OpenTUI foundation All 15 findings from the independent static review verified in source and fixed (no false positives; none deferred): - quit effect carries QuitActionReturn.messages projected to text on a notice field — ink renders them via QuittingDisplay and the payload was permanently lost (Important #1) - error and finished branches emit retry-countdown-clear like ink's handleErrorEvent/handleFinishedEvent, so a terminal event inside the countdown window no longer leaves a stale retry row (#2) - projectContextUsage renders the compaction-threshold ladder and the per-item detail sections (tools/memory/skills, ink's sort order) when showDetails is on — /context detail transcripts no longer show strictly less than the compact view (#3) - projectMcpStatus honors showSchema (parameter JSON under each tool) and showTips, so /mcp schema is distinguishable from /mcp (#4) - SlashDispatchEnv.settings is required: the real CommandContext.services.settings is non-null and a null surfaced as a generic command failure on first .merged read (#5) - dead singleColumn flag removed from the help width layout (clamp makes it always false; ink has no single-column mode) (#6) - truncated SS3 tail (bare ESC O) is stripped like the truncated CSI tail, so a captured half F1-F4 no longer leaks 'O' into the composer (#7) - readBufferRow trims cellColumns alongside text, so URLs ending at end-of-row on a wide character hit-test on both halves of the cell (#8) - kitty probe writes guarded: a synchronous stream throw settles the probe (restores raw mode, removes the listener) instead of leaking (#9) - eraseLines reuses the ansi-escapes helper (already a repo dependency) instead of a byte-identical hand-rolled copy (#10) - truncateText passthrough wrapper dropped; callers use the exported truncateHelpText directly (#11) - SkillsList truncate keeps total length n like ink, so the description column no longer shifts by one cell when a name truncates (#12) - model_fallback names pass through sanitizeDisplayText like ink (#13) - selectIndex fires onHighlight before onSelect (ink dispatches SET_ACTIVE_INDEX then SELECT_CURRENT), keeping highlight-driven stay-open dialogs synced on mouse input (#14) - mcp_app without fallbackText renders empty instead of JSON-dumping the embedded HTML; projectAbout hides Base URL when selectedAuthType is empty, matching ink's formatBaseUrl (#15) * fix(cli): address R3 review findings in OpenTUI foundation modules - executeSlashCommand catch checks the abort signal first: an ESC- cancelled command (action rejects AbortError) returns handled with no failure telemetry or error message, mirroring ink's processor — the race promise never resolves when the signal is already aborted at addEventListener time - submit effect carries the full SubmitPromptActionReturn contract (modelOverride, onComplete, refreshContextFilesOnWrite) so the backend can honor /model <id> <prompt>, /dream's manual-run record, and /remember's context refresh like ink instead of silently degrading them - closing fences cannot carry info text (CommonMark): a ```js line inside an open block is literal body, not an early close that drops the block and inverts parse state for the rest of the document - info items append their linkUrl/linkText footer (ink's InfoMessage renders it; headless/SSH users need the printed URL, e.g. /bug) - the screen-reader sanitize keeps TAB: it separates words in tool/model output and deleting it fused adjacent tokens * fix(cli): address R4 review findings in OpenTUI foundation modules - a11y-plain-text: split on all CommonMark line endings so CRLF markdown opens/closes fences correctly; private-param CSI regex covers ECMA-48 intermediate bytes; DCS/SOS/PM/APC and unterminated OSC sequences consumed before strip-ansi; code-span pattern mirrors ink's INLINE_CODE_SPAN_PATTERN (non-empty content, closing-run lookbehind) - dialogs-shared: clearNumberBuffer called from setActiveIndex, selectIndex, and resyncKey block so wheel/hover/click/resync can't commit a stale numeric-flush selection the user never made - event-adapter: tool_call_response carries visionBridgeNotice on the tool-result event (ink ToolMessage renders the egress disclosure) - item-projection: projectContextUsage reads memoryFiles as { path, tokens } (ContextMemoryDetail), not { name, tokens } - key-map: 10 kp* keypad-navigation aliases (kpleft→left, …) and super flag folded into meta (ink Cmd+Enter = newline, not submit) - link-click: cellColumns no longer truncated to trimmed text length (preserves the wide-glyph right-half boundary); findUrlAtRow end boundary is width-aware (stringWidth of the last glyph) - slash-dispatch: submit effect carries PartListUnion content + a textContent string for text-only consumers (image parts survive); toggleVimEnabled and startNewSession seams wired from env; abort race resolves immediately for an already-aborted signal - clipboard: OSC 52 self-write removed — copyToClipboard's existing fallback (writeOsc52 / wrapForMultiplexer) is the single source - a11y-screen-reader: appendStatic skips clean === '\n' (ink's hasStaticOutput guard) * fix(cli): address #10383 R1 findings in foundation modules - event-adapter: finished branch emits retry-countdown-clear BEFORE the info notice so the countdown row is actually cleared (the fold only pops when the last item is the retry row) - item-projection: /mcp tips now include all 5 lines ink renders (added OAuth auth tip and Ctrl+T toggle tip) - link-click: wide-glyph end boundary uses the last code point (not UTF-16 code unit) so non-BMP emoji are measured correctly by stringWidth - a11y-plain-text: CSI_SEQUENCE replaces PRIVATE_PARAM_CSI — drops the marker requirement so any CSI (with or without private parameter marker, with or without intermediate bytes) is fully consumed * fix(cli): address #10383 R2 Critical findings in slash-dispatch - abort race: already-aborted signal now skips command.action entirely (result = undefined) instead of eagerly evaluating it as a Promise.race argument — the action's side effects (clear, persist, addItem) must not run on a cancelled submission - parent command telemetry: logEvent (slash_command SUCCESS) is now called before the early return for parent commands with subCommands (help listing) and bare handled — matching ink's finally-block logging |
||
|
|
a12412016f |
fix(serve): surface provider error detail in turn_error messages (#10569)
* fix(serve): surface provider error detail in turn_error messages Daemon turns that failed because the model provider rejected the request published a generic "Internal error" as the turn_error message, hiding the upstream reason (e.g. engine_overloaded_error) from the Web Shell. Teach the bridge's error-message extractor to read the nested data.error shape the ACP SDK produces when the agent's error message is itself a JSON string, with the same precedence the desktop client uses. Fixes #10564 * codex: address PR review feedback (#10569) Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
f6cca89aa2 |
fix(sdk): Surface daemon JSON-RPC error details (#10571)
* fix(sdk): surface daemon JSON-RPC error details Co-authored-by: Qwen-Coder <[email protected]> * test(sdk): address PR review feedback (#10571) Co-authored-by: Qwen-Coder <[email protected]> * test(sdk): cover HTTP error detail boundaries Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
eaf3045c36 |
ci: apply ECS integration limits to release (#10592)
Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
4908f859d5 |
feat(serve): expose Workflow tasks and controls (#10411)
* feat(serve): expose workflow tasks and controls * fix(serve): align workflow capability gating * fix(cli): reject live workflow history deletion * fix(serve): start controlled workflows in background * fix(serve): address workflow control review blockers * fix(serve): make workflow history deletion race-safe * fix(serve): enforce workspace trust on the daemon Workflow surfaces (#9546) * fix(serve): close cross-session workflow deletion races and untrusted task reads (#9546) - Session history merge now treats a persisted snapshot as authoritative over a stale callback cache, and retires the cache once the runner confirms the snapshot write (new registry snapshot-persisted hook), so a sibling's deletion is not resurrected on refresh. - History deletion consults every sibling session's run registry (live entries and settling handles) before deleting from the shared store, and a successful deletion purges sibling terminal entries so retries cannot re-persist a deleted run. - Workflow holds mirror the registry's hasRunningEntries: paused runs no longer pin the session indefinitely. - The includeWorkflows opt-in is gated on workspace trust at the daemon boundary in both the ACP-HTTP dispatch and the REST tasks route, matching the fail-closed shape of the other workflow surfaces. * fix(cli): close the four cross-session workflow-history consistency holes Addresses R5-9, R7-4, R7-5 and R7-10 — all four Criticals open on this PR. They are one family: a run's history can be deleted, retried, or listed from any session, and each gate had a different idea of which runs exist. R5-9 — the mutation claim is task-global, not session-scoped. Keyed `sessionId\0taskId`, it serialized nothing that mattered: every session shares one snapshot store. A sibling's retry passed canStart (`failed`, no handle), took its own per-session claim, then awaited journal load/compile before `register()`; a delete-history landing in that structural window found the run terminal and handle-less in every registry, removed the journal directory and snapshot, and answered `{changed: true}` — after which the retry re-registered and its settlement re-persisted the history the user was told was deleted. The claim is now keyed by taskId alone and taken by delete-history, retry, rerun and run-saved. `run-saved` keys off a saved workflow's NAME, so it claims in its own `saved\0` namespace rather than colliding with runIds. R7-4 — deletion tests membership against the uncapped merged set. `buildSessionTasksStatus` serializes every registry entry unconditionally while `refreshWorkflowHistory` truncates to MAX_RETAINED_SNAPSHOTS by startTime, so a long run that settled after ~30 newer ones started stayed listed via the registry but fell out of the window — terminal, handle-free, live in no sibling, and permanently undeletable. `refreshWorkflowHistory` now records the merged id set before the cap, and deletion gates on that, the registry, or the unpersisted cache. `deleteWorkflowSnapshot` already tolerates an absent target, so the wider gate cannot delete what is not there. R7-5 — snapshot retirement is a latch. The registry's dispatch-drain callbacks emit status changes on TERMINAL entries with no status gate, and in-flight dispatches keep draining across the snapshot write, so a terminal emission routinely landed after `notifySnapshotPersisted` had retired the cache entry — re-inserting the run as "never persisted". A sibling's deletion was then undone by the next refresh, which reads "absent on disk, present in cache" as a pending write and republishes. Persistence is now remembered per runId and `#rememberWorkflowHistory` returns early for members; the latch releases when the runId goes active again, so a genuine re-run is still cached. R7-10 — the liveness gate sees runs whose session is gone. It iterated `this.sessions` only, but close/kill/shutdown use force semantics and a background run owns a detached controller, so after `removeStoredSessionEntry` a still-settling run was invisible to the gate and unreachable by the delete handler's sibling `removeTerminal` loop: a sibling delete-history removed the LIVE run's journal and snapshot, and the orphan's settlement write recreated it. Two halves — `Session.dispose()` now aborts its workflow registry the way it already aborts the agent registry (before the callbacks are torn down), and the registry of a removed session is retained here until its runs drain, so the gate still answers across the settlement window an abort cannot compress to zero. Retention is bookkeeping: a Config that cannot answer is logged, never turned into a shutdown failure. Regressions, each mutation-checked against its own repair: - acpAgent: a sibling's parked retry makes delete-history answer `{changed: false}` without reaching the store, and the deletion goes through once the claim releases - acpAgent: a live run's registry stays visible to the gate across its session's close, and is dropped once the handle is released - Session: a status emission after `snapshotPersisted` no longer resurrects a sibling-deleted run (reverting the latch reproduces the reviewer's probe verbatim), and a re-registered runId is remembered again - Session: a run with the oldest startTime behind 30 newer snapshots is deletable - Session: dispose aborts the workflow registry before clearing its callbacks Verification: Session.test.ts 733 passed, acpAgent.test.ts 514 passed, `tsc --noEmit -p packages/cli` clean, eslint and prettier clean. The 14 `packages/cli/src/serve` failures (fast-path import boundary, capabilities-docs contract, workspace fs/agents/memory, conversation runtime ownership) reproduce identically on the unmodified head — base skew, untouched by this change. Claude-Session: https://claude.ai/code/session_01M7z4PccYfDPyyfg3oGr8V1 * fix(core): drop the duplicated telemetry-swap mock property Merging upstream/main ( |
||
|
|
b58f5c0692 |
docs(serve): document tool PATH for managed daemons (#10576)
Co-authored-by: Qwen-Coder <[email protected]> |