Commit Graph
9169 Commits
Author SHA1 Message Date
ChiGao 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 656e996c9a implements a cursorOffset
getter/setter but the interface it is cast through never declared the
member, so tsc --build fails with TS2339 at the two reads in the
wide-char placeholder test. Typecheck ran before that commit's files
were staged and missed it.

* test(cli): stub listStartingRunIds in the session-switch fake registry

The workflow-run registry gained listStartingRunIds with the workflow
tasks feature on main; backgroundWorkUtils iterates it when describing
blocking work, so the fake registry in session-switch.test.ts now
implements it (empty) to match the interface the merged code expects.

* fix(opentui): address yiliang114 review findings (4 P2 + 1 P3)

- transcript-adapter: FIFO queue for id-less tool call pairing so
  tool-start and tool_result share the same minted id
- session-switch: move uiSwapped=true to right after startNewSession
  (the first irreversible host mutation) preventing core/UI divergence
  on mid-sequence throw; same fix for branch handler
- session-switch: add error item when /resume targets an unloadable
  session instead of returning silently
- diff-render: run diff content through escapeAnsiCtrlCodes matching
  the ink text-boundary convention (useTurnDiffs.ts)
- package.json: move remend from devDependencies to dependencies
  (imported from production source markdown-heal.ts)

* fix(transcript): address R6 review — thinking latch, cancelled status, text join

- Replace one-shot `closed` latch with `thinkingOpen` state so
  [thought, text, thought, ...] patterns emit matching thinking-end
  for each burst (P2)
- Mirror live path: treat cancelled tool status as failed, not ok (P3)
- Join user text parts with newline instead of empty string (P3)
- Regenerate package-lock.json so remend is in dependencies (P2)

* fix(opentui): address review-pr bot R3 critical findings

- session-compaction: add missing COMPRESSION_FAILED_EMPTY_SUMMARY,
  OUTPUT_TRUNCATED, and API_ERROR cases to match ink compression-text.ts
- live-session: distinguish cancelled from error in tool-end summary
  so toolStatusMeta renders strikethrough instead of red X
- transcript-adapter: gate slash_command replay on phase=invocation to
  prevent double-replay (recorder writes both invocation and result)
- input-prompt: add key.meta/key.option to DELETE_WORD_BACKWARD branch
  to match the guard condition that intercepts Alt+Backspace

* fix(opentui): address round-5 review findings R2-5 R3-2 R3-12 R4-1

R2-5: update session-compaction.test.ts to assert the three new
parity texts (EMPTY_SUMMARY, OUTPUT_TRUNCATED, API_ERROR) added in
63a7f3c; the old assertion that EMPTY_SUMMARY returned '' is stale.

R3-2: transcript-adapter replay producer folded cancelled tool status
into summary 'error' (red ✕) instead of 'cancelled' (strikethrough).
Add the cancelled branch to match live-session.ts.

R3-12: hidden slash-command invocations (hiddenInvocation: true for
/auth, /help, /settings, /status, bare /effort, /btw) replayed as
visible user rows and entered composer history. Gate them on the
hiddenInvocation flag in the invocation filter.

R4-1: modelOverride was only carried on the first UserQuery send;
ToolResult continuation sends omitted it, so a per-turn model override
silently reverted to the session default after the first tool batch.
Propagate modelOverride into every continuation send.
2026-09-01 02:13:36 +00:00
qqqysandqqqys 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]>
2026-09-01 02:13:22 +00:00
Kevin Wang 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.
2026-09-01 01:41:16 +00:00
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]>
2026-09-01 01:38:15 +00:00
Shaojin Wen 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.
2026-09-01 01:35:04 +00:00
Shaojin Wenandqwen-code-dev-bot 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]>
2026-09-01 01:18:58 +00:00
Shaojin Wen 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.
2026-09-01 00:55:03 +00:00
ChiGao 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.
2026-09-01 00:51:03 +00:00
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 (1ac2a8104e): request counts and
result evidence were fed from different populations. Turn yields one
ToolCallRequest event per raw streamed function call (no dedup), so a
provider-emitted duplicate call id advanced the consecutive-identical
counter twice, while execution collapses duplicates (scheduler
dedupeRequestsByCallId / interactive duplicate-call-id suppression) and
recordToolResultByCallId consumes once per call id — leaving the request
counter permanently one ahead of the result evidence within a streak.
The result-aware exemption then failed safe and halted a productive
task_list poller whose board changed on every poll.

The main-session stream loop now feeds the loop guards (always-on and
heuristic tiers) one event per call id per attempt — a per-attempt Set
cleared on retry/model-fallback alongside the attempt's accumulated
state, mirroring dedupeRequestsByCallId (id-less requests are never
deduped). The events themselves still flow to stream consumers; only the
guard feed is deduped.

* fix(core): carry the inner stub digest through batch-budget fitting (#9450)

* fix(core): decay abandoned stateful streaks at round-trip boundaries (#9450)

* fix(cli): decay abandoned stateful streaks at daemon batch boundaries (#9450)

* fix(core): make the alternating-pattern carve-out in-flight aware (#9450)

* fix(core): keep degenerate batch-budget fits content-dependent (#9450)

* fix(core): exclude never-executed synthetic results from the subagent loop guard (#9450)

* fix(core): exclude synthetic duplicate responses from the main-session loop guard (#9450)

* fix(cli): skip daemon abandonment decay for batches that execute nothing (#9450)

* fix(core): canonicalize loop-guard fingerprints across the batch-budget fit (#9450)

* fix(core): unwind suppressed replays and skip decay for requested keys (#9450)

* fix(cli): skip daemon batch decay for re-requested stateful keys (#9450)

* fix(core): keep sub-label batch-budget fits content-dependent (#9450)

* test(core): align the replay-in-streak halt with the suppression unwind (#9450)

* fix(core): make the always-on consecutive guard in-flight-aware for parallel stateful batches (#9450)

The exoneration gate assumed the prior N-1 results of the Nth identical
request had all been recorded, but with parallel tool batches ALL of a
round's identical task_list requests stream through the guard before ANY
of that round's results lands (dedupeToolCallsById collapses only
same-callId duplicates). The gate became unsatisfiable and a productive
changing-board poller halted CONSECUTIVE_IDENTICAL_TOOL_CALLS — the #9450
false positive re-entering via a parallel batch.

Give the always-on guard the same per-key in-flight accounting the
alternating-pattern carve-out already uses: maintain statefulInFlight in
the always-on path (checkAlwaysOnSafeties) so it also works under the
skipLoopDetection default, and judge the gate on
toolCallRepetitionCount - inFlight results (floored at the recorded
evidence). A genuine wiring gap (shortfall with no recorded evidence)
still fails safe and halts, preserving the #5019 protection.

* fix(core): reset the consecutive streak when its result evidence decays (#9450)

The Finished-boundary decay zeroed a key's result evidence
(resultsObserved / unchangedStreak) after two consecutive mark-less
round-trips, but the always-on consecutive streak survived untouched.
When polling resumed mid-streak, the exoneration gate could never be
satisfied again (resultsObserved can only ever reach count - 2), so a
changing-board poller halted CONSECUTIVE_IDENTICAL_TOOL_CALLS — the
#9450 false positive re-entering via the decay layer.

Drop the consecutive streak together with its evidence in
decayAbandonedStatefulStreaks so resumed polling starts a fresh streak
judged on its own results. Decay never runs for a key with requests
still in flight (the requested-set skip), so this cannot defeat the
in-flight deferral.

* fix(cli): mark suppressed stateful replays for the daemon batch decay (#9450)

A MIXED daemon batch — a suppressed task_list replay alongside at least
one executable call — wiped the frozen-board streak: requestedStatefulKeys
is built from executable calls only, the batch is non-empty so the
empty-batch early return does not apply, and once the prior result's mark
was consumed the replayed key sat in neither skip set, so
decayAbandonedDaemonStreaks zeroed the streak and recomputed
statefulMaxResultRepeat to 0. With wipes landing every few executed polls
the stuck signal never reached GLOBAL_DUPLICATE_THRESHOLD and the
adaptive-cap halt never fired under CLI-default skipLoopDetection=true —
the two runtimes drifted (core marks suppression via
noteSuppressedToolCallByCallId; issue #9450 requirement #6).

Mirror core's suppression mark in pushDuplicateBatch: add the replayed
stateful key to statefulResultKeysSinceLastBatch before
recordDaemonToolCalls runs, and add a mixed-batch variant of the
replay-interleave regression test.

* fix(cli): exclude never-executed skipped-output synthetics from the loop guards (#9450)

nonInteractiveCli's sibling suppression fabricates skipped-output
responses for unexecuted calls (a structured_output call sharing a batch
with task_list polls in a --json-schema headless run). The synthetic
parts carry the original callId, fail isDuplicateProviderToolCallResponse
and carry no executionStatus, so client.ts's recording feed paired them
with the streamed requests: the constant fabricated fingerprint counted
as a "changed" result every round, exonerating a stuck frozen-board
poller round after round. The daemon twin excludes exactly this class
(providerDuplicate / not_started filter) and agent-core excludes it via
neverExecutedCallIds.

Mark each unexecuted sibling call via
LoopDetectionService.noteSuppressedToolCallByCallId at synthesis time:
the request-side reservations unwind and the later fabricated response
finds no pairing, mirroring the daemon's not_started filter (issue #9450
requirement #6).

* fix(core): reduce digest-carrying stub shapes the prefix list does not enumerate (#9450)

* fix(cli): unwind the loop-guard reservations for plan-mode sibling skips (#9450)

* fix(cli): protect the suppressed replay key across the next daemon batch boundary (#9450)

* fix(core): keep the loop attribution on the final agent task card (#9450)

* fix(core): return the full digest line from degenerate batch-budget fits (#9450)

* fix(core): carry stateful streak marks across replay-suppressed rounds (#9450)

* fix(core): keep rejected stateful calls counting toward the consecutive-identical guard (#9450)

* fix(cli): unwind never-executed not_started synthetics before the interactive feed (#9450)

* fix(core): clear stateful loop-guard trackers on model fallback (#9450)

* fix(core): keep suppressed request counts when decay keeps the streak (#9450)

* fix(core): share one stub grammar between fitText and the loop guards (#9450)

* test(core): realign the replay-in-streak halt pin with the kept suppression count (#9450)

The keep-suppressed-counts fix replaced the suppression unwind of the
consecutive-identical increment: a suppressed call now keeps its
request-side increment and is counted into the streak's
suppressedRequests, which the exoneration gate subtracts from the
expected results. The replayed poll therefore still counts toward the
threshold, so the frozen-streak halt lands when poll_4 streams in as
the fifth identical request — one round earlier than under the unwind
the previous pin tracked: expectedResults is 5 - 1 in flight (poll_4)
- 1 suppressed (the replay) = 3, exactly the three unchanged
frozen-board results recorded for poll_1..poll_3, so the halt is
corroborated by executed evidence at three executions. Mutation
checks: reverting to unwind semantics moves the halt back to poll_5
(four executions), and recording the fabricated replay response as a
result disarms the halt entirely (five executions).

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

* fix(cli): carry live stateful streak marks across non-stateful replay suppressions (#9450)

The daemon's replay-suppression marks (pushDuplicateBatch /
emitDuplicateBatch) re-added the replayed key only when the replay
itself was stateful, while core's twin
(carryStatefulStreakMarksAcrossSuppression) re-adds every live streak
key on ANY replay suppression. A MIXED batch whose suppressed replay
is NON-stateful (the provider re-emitting an already-handled
read_file-style call id alongside an executable call) therefore left
the polled task_list key in neither skip set once the previous poll's
result mark was consumed: decayAbandonedDaemonStreaks wiped the live
frozen-board streak at the next boundary and recomputed
statefulMaxResultRepeat toward zero. Under the CLI default
skipLoopDetection=true — where the cap's stateful stuck signal is the
only live halt path — replays interleaved at <=5-poll intervals kept
the peak below GLOBAL_DUPLICATE_THRESHOLD indefinitely and the turn
ran toward the hard backstop, while core halts the identical event
sequence just past the soft cap (requirement #6).

Mirror core's carry: on any replay suppression, re-add every
statefulResultStreaks key with consecutiveIdenticalResults > 0 to
statefulResultKeysSinceLastBatch, in both pushDuplicateBatch (the
batch's own boundary) and emitDuplicateBatch (the next boundary).
Decayed streaks carry zero, so the carry only postpones an imminent
decay by one boundary — it never resurrects an abandoned peak. Adds a
Session.test.ts interleaving pinning the exact mixed non-stateful
replay shape (frozen board, poll every 3rd round, halt at
totalToolCalls 21); the pin fails with the carry reverted.

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

* refactor(core): trim PR back to the core result-aware loop-guard fix (#9450)

The PR accreted 53 commits / +7634 lines over a week of review rounds,
each adding speculative edge-case guards and tests far beyond the
reported issue. This commit restores the PR to the original fix scope:

- Keep the result-aware duplicate-detection core in loopDetectionService
  plus its attribution plumbing (client, agent-core, agent-headless,
  agent-interactive, agent-events, telemetry) and the matching tests,
  three-way merged onto current main.
- Revert the 17 files that only carried accreted hardening (daemon
  Session path, truncation fingerprinting, tool-response-finalizer,
  synthetic-result exclusions, batch-budget fitting, decay logic, and
  their tests) to the main baseline.

Net PR diff: 10 files, +1051/-14 (was 27 files, +7634/-44).

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

* fix(core): count consecutive results for stateful poll loop guards (#9450)

The result-aware counting for stateful read tools accumulated turn-wide
(call, result fingerprint) pair totals, contradicting the invariant its
own comment states: the same call returning changed state is productive
and must not accumulate toward either halt. A board oscillating between
two byte-identical states repeats each (call, result) pair across the
turn, so it halted with GLOBAL_TOOL_CALL_DUPLICATE on the 11th poll with
heuristics on, and — under the CLI default (skipLoopDetection) — the
pair totals fed capMaxKeyRepeat until the always-on adaptive cap halted
with TURN_TOOL_CALL_CAP past the soft cap.

Count consecutive identical results per repeat key instead: the count
restarts at 1 whenever the result differs from its predecessor, so an
oscillating board is changed-state progress on every poll in both modes
while a frozen board still accumulates even when interleaved with other
calls. Adds regression tests pinning oscillating-board survival in both
modes and the interleaved frozen-board halt via the adaptive cap.

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

* fix(core): restore the trimmed verification findings for stateful poll guards (#9450)

The sandboxed verification of the trimmed PR reported four findings; this
restores the three behavior fixes it proved live and pins the four coverage
gaps its mutation matrix exposed:

- Oversized (persisted) results escaped every result-aware guard: the
  stub envelope embeds a per-call unique path, so hashing it verbatim
  fingerprinted uniquely every poll and a frozen board read as "changed
  every time" (probe P2: no halt in 12 polls vs base halting at 5).
  buildStub now embeds a sha256 of the full pre-truncation output
  ("Full output sha256: <hex>"), and the guard's fingerprint reduces
  leading-producer stubs to that digest — with a path-free preview/
  truncated-part payload fallback for digest-less stubs and verbatim
  treatment of non-stub text (a mid-content quoted marker never matches:
  recognition is prefix-gated and the digest label must start its line).
  Frozen oversized boards halt at the unchanged threshold regardless of
  the per-call path; mutations past the preview window stay visible.
- Provider-duplicate call ids halted a productive poller fail-safe (probe
  P3): request counts fed pre-dedup while results land once per deduped
  executed call. Both reasoning-loop owners now feed the guards one
  ToolCallRequest per call id per attempt (a per-attempt Set mirroring
  dedupeToolCallsById; id-less calls never deduped), cleared on
  retry/fallback — agent-core's subagent stream loop and client.ts's
  main-session stream.
- Coverage pins (finding F4): restored the client.ts recording-wiring
  tests (frozen halt, changed survival, duplicate-id population parity);
  added QwenLogger loop_type journal tests, the interactive stop-message
  attribution test, and the partial-evidence fail-safe fixture.

Verification: packages/core tsc clean; loopDetectionService 145/145,
agent-headless 68/68, client 370/370, agent-interactive 25/25,
qwen-logger 43/43, truncation/shell/finalizer 368/368; prettier/eslint
clean on touched files.

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com>
Co-authored-by: Shaojin Wen <[email protected]>
2026-08-31 23:48:13 +00:00
易良andQwen-Coder 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]>
2026-08-31 23:40:57 +00:00
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]>
2026-08-31 23:39:06 +00:00
易良andQwen-Coder 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]>
2026-08-31 23:37:07 +00:00
Shaojin Wen 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.
2026-08-31 19:31:03 +00:00
易良andQwen-Coder 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]>
2026-08-31 17:46:54 +00:00
bluefateludi 6828ac7829 fix(cli): detect overlapping review ranges (#9801) 2026-08-31 17:24:44 +00:00
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]>
2026-08-31 17:13:36 +00:00
易良andQwen-Coder 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]>
2026-08-31 17:02:18 +00:00
jinyeandQwen-Coder 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]>
2026-08-31 16:53:25 +00:00
易良andQwen-Coder 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]>
2026-08-31 16:25:18 +00:00
ChiGao 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.
2026-08-31 14:56:05 +00:00
易良andQwen-Coder 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]>
2026-08-31 13:32:18 +00:00
dreamWBand易良 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]>
2026-08-31 13:16:56 +00:00
jinyeandQwen-Coder 41cdc4f99a feat(external-context): load administrator-owned Mem0 dialects (#10634)
Co-authored-by: Qwen-Coder <[email protected]>
2026-08-31 13:13:43 +00:00
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]>
2026-08-31 13:08:42 +00:00
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]>
2026-08-31 12:29:27 +00:00
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]>
2026-08-31 12:18:54 +00:00
易良andQwen-Coder 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]>
2026-08-31 12:18:44 +00:00
易良andQwen-Coder f772343c78 ci: update ECS runner fleet labels (#10629)
Co-authored-by: Qwen-Coder <[email protected]>
2026-08-31 12:07:32 +00:00
易良andQwen-Coder 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]>
2026-08-31 11:58:07 +00:00
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]>
2026-08-31 11:50:45 +00:00
易良andQwen-Coder 2268c9bc06 fix(sdk): make daemon bundle size advisory (#10630)
Co-authored-by: Qwen-Coder <[email protected]>
2026-08-31 11:48:39 +00:00
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]>
2026-08-31 11:40:46 +00:00
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]>
2026-08-31 09:57:46 +00:00
callmeYe 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
2026-08-31 09:48:05 +00:00
ytahdnand钉萁 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]>
2026-08-31 09:45:40 +00:00
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 cde150c9 re-homed the replay into
publish-resolution in the workflow but left the 'recovering requests that
used to be lost' test block sourcing `reportStep` (and the pushed-tree
upload, and the env-coverage walk) from resolve-pr, where 'Report result'
no longer exists — so reportStep was '' and every replay assertion and
fixture in that block failed.

Source the replay/push/upload assertions from publish-resolution, keep
prepare/authorize on resolve-pr, walk both jobs in the uppercase-variable
env-coverage test (the replay's BASE_REF lives in publish now, from
needs.resolve-pr.outputs), and match the pushed-tree artifact name
(needs.resolve-pr.outputs.pr_number) and its always() gate regardless of
quote style. 53 pass; mutation-checked (breaking the lease move or the
untouched-file guard in replay_on_moved_head reds the fixtures).

---------

Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
2026-08-31 09:17:44 +00:00
Kewenand恪文 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]>
2026-08-31 09:09:34 +00:00
qwen-code-dev-bot 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)
2026-08-31 08:59:48 +00:00
易良andQwen-Coder 6608cd3b3d fix(ci): coordinate Docker sandbox workloads on shared hosts (#10605)
Co-authored-by: Qwen-Coder <[email protected]>
2026-08-31 08:43:52 +00:00
jinyeandQwen-Coder 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]>
2026-08-31 08:31:11 +00:00
易良andQwen-Coder be5f00eb80 test: harden release timing-sensitive waits (#10615)
Co-authored-by: Qwen-Coder <[email protected]>
2026-08-31 08:02:42 +00:00
Shaojin Wenandqwen-code-dev-bot 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]>
2026-08-31 07:37:15 +00:00
易良andQwen-Coder 88a136317a fix(ci): scope release runner context to steps (#10598)
Co-authored-by: Qwen-Coder <[email protected]>
2026-08-31 05:37:07 +00:00
ChiGao 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.
2026-08-31 05:00:49 +00:00
ChiGao 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
2026-08-31 03:48:37 +00:00
jinyeandQwen-Coder 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]>
2026-08-31 03:12:38 +00:00
jinyeandQwen-Coder 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]>
2026-08-31 02:52:24 +00:00
易良andQwen-Coder eaf3045c36 ci: apply ECS integration limits to release (#10592)
Co-authored-by: Qwen-Coder <[email protected]>
2026-08-31 02:52:23 +00:00
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 (053f17b775) clears the 9 `client.telemetrySwap`
failures this branch had from predating #10220, but main carries its own
TS1117 on the same file: two commits added `getToolRegistry` to the same
object literal independently and neither saw the other.

  032b9078e6 feat(serve): backfill session PR bindings ... (#9729)
  8241905392 test(core): give the telemetry-swap client mock a
             getToolRegistry (#10220)

Checking that file out from upstream/main here and running
`tsc --noEmit -p packages/core` reproduces
`client.telemetrySwap.test.ts(103,5): error TS1117` verbatim, so taking
the merge unmodified would have traded 9 test failures for a build that
never reaches the tests at all.

Removed as part of the merge rather than left for a follow-up: keeps
#10220's copy, which was added for this purpose and carries the
explanation, and drops #9729's incidental one. main still needs the same
removal — this only keeps it out of the branch.

Verified after the merge: `tsc --noEmit` clean for both packages/core
and packages/cli; client.telemetrySwap 10 passed, Session.test.ts and
acpAgent.test.ts 1260 passed together.

Claude-Session: https://claude.ai/code/session_01M7z4PccYfDPyyfg3oGr8V1

* fix(workflows): guard pending run lifecycle

* fix(workflows): let a starting run be cancelled, and stop two slow leaks

Four of the behavioural items the review deferred across rounds 8-9; the
test-coverage-only entries stay deferred.

- Cancel during the start window (R9 acpAgent.ts:10884 + workflow-runner.ts:181).
  Between `reserveStart` and `register` the runner loads the script and
  replays the journal — seconds, for a resume of a large one — and the
  registry has no entry yet. `sessionTaskCancel` answered `not_found` for a
  run the client could see starting, and `registry.cancel` could not reach
  the reserved controller either. New `cancelStarting` aborts it (the
  reservation stays the runner's to release, as after `abortAll`), and the
  cancel handler routes there when the liveness gate would say "starting".
  Doing that exposed the second half: the runner threw a bare `Error` for
  an abort during start, and the tool's catch only recognised the CALLER's
  signal — a registry-side abort surfaced as an unexplained failure. It is
  now a typed `WorkflowStartCancelledError`, mapped to the same
  "cancelled before it could start" result.
- `detachedWorkflowRegistries` (R8 acpAgent.ts:3386) was pruned only inside
  the delete-history liveness check. A daemon that closes sessions mid-run
  and never deletes history retained every registry for its lifetime. Prune
  on session close as well.
- Refresh/delete race (R9 Session.ts:3428). `refreshWorkflowHistory` reads
  the directory and merges without a claim; a delete landing between the
  read and the merge was overwritten by the stale listing and the run
  reappeared until the next refresh. Deletions are now sequenced, and a
  refresh drops any run deleted after its read began — keyed by runId and
  compared against the refresh's own mark, so a later retry that reuses
  the id is not suppressed.
- The "Register a new run" JSDoc sat on `reserveStart` (R9 registry:594).

Mutation-verified, all four at once against the full suites: disabling the
starting-window branch, the prune-on-close, the deletion filter, and the
typed-error mapping reddens exactly the four new tests and nothing else.

Claude-Session: https://claude.ai/code/session_01VXsC4f71S6U6YkW82NRw7m

* fix(core): close the round-5 review findings on workflow task controls

- Key the starting-window cancel on a live reservation rather than on the
  absence of an entry: a retry reuses its runId, so its terminal entry
  shadowed the reservation and cancel answered `not_running` about a run
  that was actively starting.
- Answer `changed: false` from retry when `execute()` reports a start that
  never registered (no `workflowRunId`), mirroring rerun and run-saved.
- Classify a registry-side abort of the reserved controller as a cancel in
  foreground starts too, not only background ones; the tool maps
  `WorkflowStartCancelledError` in either mode.
- Report reserved-but-unregistered runs as workflow active-work holds
  (`WorkflowRunRegistry.listStartingRunIds`), so a daemon conditional close
  cannot dispose the session under a start it just accepted.
- Propagate a successful history deletion into every sibling session's
  deletion marker and cached history, symmetric to the `removeTerminal`
  sweep, so a sibling refresh that had already read the directory cannot
  republish the deleted run.

Claude-Session: https://claude.ai/code/session_018dYE4LwSMeMPFchXk5UBdM

* fix(serve): keep workflow retry and the workflowsEnabled flag consistent across sessions

Two cross-session gaps in the workflow control surface:

- A retry consulted only the requesting session's registry. Every
  session shares one journal/snapshot store and the task-global claim is
  released as soon as the background start returns, so a sibling whose
  registry still showed the run `failed` started a second runner under
  the same runId. Retry now refuses while the runId is live in any
  session (or in its own starting window), checked synchronously beside
  canStart so the answer cannot go stale before the claim is taken.

- Workspace reload updated `tools.workflowsEnabled` for `/capabilities`
  but never told existing sessions; `Config.workflowsEnabled` was set
  once at construction. The reload's `tools` branch now propagates the
  flag and pushes an available-commands update when it flips.

Claude-Session: https://claude.ai/code/session_01NkW1J2aBKcsKS62dkPcWbT

* fix(serve): refuse to report a workflow history deletion whose registry entry survived

`deleteWorkflowHistory` ignored `removeTerminal()`'s answer. The registry
refuses to remove a live or handle-held entry — its own last word on
whether the run is still active in this session — so a `false` for an
entry that exists meant the run re-registered under the deletion and
would re-persist the history the client was just told was gone. The
entry is now retired before the store is touched, and a refusal fails
the deletion; a persisted-only run has no entry and is unaffected.

Claude-Session: https://claude.ai/code/session_01NkW1J2aBKcsKS62dkPcWbT

* docs(serve): fix a stale method reference in the workflow history comment

The persisted latch is released inline at the top of #rememberWorkflowHistory;
no #forgetPersistedWorkflowRun exists.

Claude-Session: https://claude.ai/code/session_01NkW1J2aBKcsKS62dkPcWbT

* fix: report workflow lifecycle changes

* fix(core): repair the workflow test doubles and converge every eviction

The previous commit added a `listStartingRunIds()` call to
`describeBlockingBackgroundWork` without teaching the registry test
doubles about it, so seven tests across `clearCommand`,
`useBranchCommand` and `useResumeCommand` threw
`listStartingRunIds is not a function`. Add the reader to those stubs.

Also move the eviction's status emission from `releaseHandle` into
`evictTerminal` itself. Emitting only from `releaseHandle` left the
same convergence gap at the four sweeping call sites that already
emit: complete / fail / cancel / abortAll emit BEFORE they sweep, so a
consumer that re-reads on the callback observes the pre-eviction list
and keeps rendering a row that was just dropped. Emitting once after
the sweep closes all five paths, and a future eviction site inherits
the guarantee. New test pins the ordering; another pins that a release
which evicts nothing stays silent.

A starting reservation now reports `starting` rather than borrowing
`running`, and its line no longer repeats the run id the bracket
already carries.

Claude-Session: https://claude.ai/code/session_01VENc5rZYmMJDdqvjwBEyZd

* fix(cli): drop the duplicated mock keys and stop misdirecting a starting run

R3-2: the previous commit's stub plumbing re-inserted a `list:` line into
four `clearCommand.test.ts` registry mocks that already declared one.
Last-wins made it behaviour-neutral, and neither eslint (`no-dupe-keys`
is off here) nor tsc (the literal is not contextually typed, unlike the
`useResumeCommand` mocks where TS1117 did fire) caught it. Remove the
duplicates.

R3-1: when the only blocker is a reserved-but-unregistered run, the
blocked message ended with "Use /workflows to inspect them, then retry."
— but `/workflows` renders `registry.list()`, which a reservation has
not entered, so it named a surface that cannot show what is blocking.
Track inspectability separately and fall back to a plain retry hint when
nothing is listable; a registered run in the same set still points at
`/workflows`.

Claude-Session: https://claude.ai/code/session_01VENc5rZYmMJDdqvjwBEyZd

* fix(cli): preserve starting workflow guidance

* test(cli): pin background work hint guards

* fix(serve): redact untrusted standalone workflow replay

* test(web-shell): fix daemon SDK mock path

---------

Co-authored-by: qqqys <[email protected]>
Co-authored-by: Qwen Code Autofix <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
2026-08-31 02:37:47 +00:00
jinyeandQwen-Coder b58f5c0692 docs(serve): document tool PATH for managed daemons (#10576)
Co-authored-by: Qwen-Coder <[email protected]>
2026-08-31 02:32:58 +00:00