- positionInsideFence now skips lines inside invoke block ranges so
fence-like content in parameter values (e.g. an edit old_string
containing triple backticks) no longer causes later invoke blocks
to be silently skipped
- Decode the five predefined XML entities (< > & "
') in parameter values so recovered args match the literal
text the model intended
- Support single-quoted attribute values in invoke/parameter tags
- Strip empty <function_calls> wrapper tags from remainingText
- Include recovered tool names and contentLength in the success log
* fix(serve): allow bounded reads of large text files
* fix(serve): bound large-text reads by scan cost, not by which knob was set
Follow-up to the bounded large-text read path. Three changes:
Gate on any explicit window argument, not on `limit`. Gating on `limit`
had the cost model backwards in both directions: `{ line: 900_000_000,
limit: 20 }` was admitted despite walking the whole file, while
`{ maxBytes: 4096 }` — satisfiable from the first 4 KiB — was refused. A
read with no window argument at all still fails, since a caller that
believes it holds the whole file may write it back truncated.
Add MAX_TEXT_SCAN_BYTES (8 MiB). MAX_READ_BYTES caps what a read
returns; nothing capped what it cost. Line offsets are resolved by
scanning from byte 0, so a query param could turn into an
uninterruptible multi-second scan of an arbitrarily large file — and on
Windows hold a read handle for that span, blocking renames and deletes.
Past the budget the read is refused with `file_too_large` pointing at
readBytes, which reaches any offset in O(1).
Tolerate appends on streamed windows. Requiring whole-file size/mtime
stability after reading a prefix rejected reads whose returned bytes
were still valid, and the case it rejected — tailing a live log — is the
one this path exists for. Streamed windows now assert inode identity
plus "did not shrink"; truncation and replacement are still rejected.
Also: non-UTF-8 large text now returns `binary_file` rather than
`file_too_large`, so a client retrying on 413 with a smaller window
can't loop forever; and `readFileWithLineAndLimit` throws instead of
silently ignoring a caller-supplied `fileHandle` on the by-path
fallback.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(core): thread the descriptor instead of forking text-read helpers
PR #7947 pinned large-text reads to one inode by threading a caller-owned
FileHandle into readTextRange as an optional field, plus a second field,
forceStreaming, to suppress the buffering fast path. Two optional fields
produced four combinations: one meaningful, one used by a single test, one
unreachable, and — in readFileWithLineAndLimit — one that silently fell
through to a by-path read, defeating the reason the caller opened a handle.
Unify the two encoding detectors. detectFileEncoding now takes a path or a
borrowed handle, so detectFileHandleEncoding is deleted along with the
message discrepancy between them: an encoding iconv-lite cannot load now
raises LargeNonUtf8TextError naming that encoding rather than deferring to
the decoder's generic invalid-utf8 variant. Both still refuse the file, and
the Serve boundary maps both to binary_file.
Split the reader into readTextRange (path) and readTextRangeFromHandle
(always streams, both byte bounds required). The unreachable combination and
its untested readFileHandleBuffer are gone, and with no fileHandle parameter
left for readFileWithLineAndLimit to ignore, the RangeError guarding that
fallthrough is deleted too — the trap can no longer be expressed.
CoreReadTextFileHandleRequest drops its required stats field. Nothing
downstream read it, and because the ACP request type it extends permits
extra properties, TypeScript accepted the dead argument silently.
readFileHandleChunks becomes chunksFromHandle(fh, from) — the one seam
byte-cursor text paging needs.
No observable change at the Serve boundary: its 222 tests pass unmodified.
Two fileSystemService tests were deleted rather than repaired; they asserted
the arguments readFileWithLineAndLimit received, which is nothing once the
handle path stops calling it. Their coverage lives in read-text-range.test.ts
against real files and in workspace-file-system.test.ts at the real boundary.
258 production lines in core, net -71 overall.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(core): make CoreReadTextFileHandleRequest standalone
Self-audit follow-up to f55c867a. Two fields survived the reshape that the
handle path never reads:
- `stats` was documented as required ("must pass the Stats captured from that
handle") and nothing downstream read it. The handle path always streams, so
it never needs a size to choose a strategy, and the encoding probe does its
own fstat.
- `path` became dead once readTextRangeFromHandle replaced the path-plus-handle
call. Errors are labelled with the path by the Serve boundary that owns it.
Neither was caught by the compiler: the ACP ReadTextFileRequest the type
derived from permits extra properties, so the CLI kept passing both silently.
That is the argument for declaring the type standalone rather than Omit-ing
four of six inherited fields and quietly re-admitting the rest.
Also record the second behaviour delta of the detector merge in the design
doc: detectFileEncoding catches I/O errors and falls back to 'utf-8', where
detectFileHandleEncoding let them propagate. The failure is not lost — a handle
that fails the 8 KiB probe fails the streaming read immediately after — but a
different call now reports it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(serve): page large text files by byte cursor
Line offsets address a byte stream, so `readText` resolves them by scanning
from byte 0. Paging a large log that way is O(n^2) across pages, and past
MAX_TEXT_SCAN_BYTES (8 MiB) a deep page is refused outright — agents had no
O(1) path short of dropping to GET /file/bytes and splitting lines themselves,
losing encoding handling, multibyte safety, and the binary_file refusal.
A response that leaves content behind now returns `hasMore`, and where a file
byte offset is derivable, an opaque `nextCursor`. Passing it back as `cursor`
resumes in O(1). Page 1 is an ordinary `limit` read, so clients never compute
byte offsets themselves, and a paging loop does not break when a file happens
to be small.
The cursor is unsigned base64url JSON carrying {off, size, dev, ino}, matching
encodeOrganizedCursor rather than the HMAC-signed transcript codec: the path is
re-resolved through the workspace boundary on every request, so a forged cursor
can only move the offset within a file the caller may already read — what
GET /file/bytes?offset= allows today. What the payload is for is staleness:
a replaced or truncated file yields hash_mismatch instead of bytes from the
wrong place, while an append leaves an outstanding cursor valid — the case the
feature exists for.
Every minted cursor points at the start of a line. When a single line exceeds
maxOutputBytes the reader emits a truncated prefix and skips to the next line
rather than resuming mid-line, because a mid-line cursor makes the following
page snap forward and silently drop the rest of that line at the seam. Windows
cut mid-line by a byte cap therefore report hasMore with no cursor, as do
non-UTF-8 snapshot reads whose decoded text is a UTF-8 re-encoding with no
mapping back to file offsets. That is why hasMore is a field rather than a
restatement of nextCursor.
Cursor reads branch before the size check, not by widening the window gate:
a cursor read of a file under MAX_READ_BYTES would otherwise land on the
snapshot path, which knows only line/limit, and silently return line 0.
Adds the workspace_file_read_cursor capability, per the convention that new
behavior gets a new tag, and retargets the scan-budget hint at cursor paging.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(core): advance UTF-8 cursors after truncation
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* docs(serve): clarify cursor bootstrap limits
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(sdk): raise daemon browser bundle budget
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* test(serve): cover ACP cursor dispatch and cursor binary_file mapping (#8002)
* fix(core): only set sawCrlf for emitted lines in cursor paging (#8002)
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
* feat(core): tag UserPromptSubmit hook context and record display provenance
UserPromptSubmit additionalContext was appended to the request as a bare
text part and persisted verbatim, so hook-injected text was
indistinguishable from user-authored text in the transcript, polluted
resumed sessions, telemetry, and auto-memory recall queries.
- Wrap injected context in a reserved
<qwen:user-prompt-submit-context> tag (hook output already escapes
angle brackets, so the tag cannot be forged from inside).
- Record the pre-injection user prompt as systemPayload.displayText plus
the injected string as hookContext on the user record; the model-bound
message stays verbatim for faithful resume replay.
- Use the pre-injection prompt text for telemetry prompt attributes and
managed auto-memory recall.
- Resume projection prefers displayText, strips a trailing whole-part
tagged block when no payload exists, and leaves legacy bare-injected
records unchanged.
- Apply the same tag wrapping on the ACP session injection path, which
already records the pre-injection prompt.
Closes#7940
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs: note UPS promptText TDZ ordering and sole-part resume guard
Document the conflict-resolution constraint that promptText must be
declared before the injection assignment, and the sole-part read-path
guard that keeps a user-authored whole-tag message intact.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(cli): cover at_command resume with tagged UPS context
Confirm the at_command branch still prefers payload.userText when a
paired user record carries a trailing tagged hook-context part, and
falls back to the tag-stripping projection only when userText is absent.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(core): address PR 7956 review findings and Goal recording spy
Omit the optional UserPromptRecordPayload third arg when no hook
injected, so Goal admission spies expecting two args stay exact and
CI client-goal.test.ts passes.
Project plain UserPromptSubmit-augmented records through
transcript-replay with the same displayText / trailing-tag strip
fallback as the TUI, covering ACP/export surfaces. Strengthen the
displayText preference fixture so it disagrees with the tag-strip
path, and use the named UserPromptRecordPayload type in resume.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(acp-bridge): import UPS tag helper via Node-free package export
transcript-replay is inlined into the browser daemon/transcript SDK
bundle. Importing isUserPromptSubmitContextPartText from the core
package barrel pulled the whole Node-bound core graph into that
bundle and failed CI (esbuild Could not resolve "node:*") across
Test, web-shell E2E, and Real daemon E2E.
Export the pure helper as @qwen-code/qwen-code-core/userPromptSubmitContext
and import that path instead.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(test): alias userPromptSubmitContext for Vitest source resolution
CLI and acp-bridge Vitest configs already map goalWire/transcriptRecords
to TypeScript sources; without the same alias the new package export
fails import analysis and breaks dozens of CLI suites.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(acp-bridge): keep images when projecting displayText user records
Preferring UserPromptSubmit displayText previously returned early and
skipped projectMessageParts, dropping multimodal inlineData. Rebuild
parts so displayText replaces text while images keep their order.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(core): drop unused hookContext and cover image-only displayText
UserPromptRecordPayload.hookContext had no read sites; keep displayText
only and recover injected text from the tagged message part. Also cover
the image-only !replaced append path and simplify the recording guard.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test: cover remaining UserPromptSubmit provenance Suggestions
Share stripTrailingUserPromptSubmitContextPart between TUI resume and
ACP replay, assert ACP Session tags additionalContext, and lock
telemetry to the pre-injection prompt text.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(cli): stamp QWEN_CODE_CLI at the workspace entry and publish the active model as QWEN_CODE_MODEL
Skill subprocesses shell out through `"${QWEN_CODE_CLI:-qwen}"`. The npm entry
(scripts/cli-entry.js) stamps QWEN_CODE_CLI, but the workspace entry
(packages/cli/dist/index.js) never did — so a dev run, or any direct
`node dist/index.js` launch, leaves the variable unset and every review
subcommand the /review skill issues silently lands in whatever global `qwen`
PATH resolves to. Measured on a live run: a freshly built CLI's review pipeline
executed entirely on a global v0.21.0 — `script-lint` did not exist there, so
the deterministic gate the skill expected was silently absent, and any
behavioral fix to the review CLI is inert in such runs.
Stamp the entry in runCliEntryPoint, first-writer-wins: an outer launcher
(cli-entry.js, the desktop shim) has already stamped in-process and must keep
winning; an empty value counts as unset, matching the consumer's `:-`
semantics. The entry is derived as `../index.js` from the compiled
dist/src/cli.js — the shebang-bearing bin — and skipped entirely for non-file
schemes (vitest) and unbuilt layouts (tsx dev runs keep today's fallback).
tsc emits dist/index.js as 0644 and the spawn-time filter blanks a
non-execable entry, so the stamp grants 0o755 best-effort; a failed chmod
degrades to today's `qwen` fallback.
Separately, subprocesses had no authoritative way to learn the ACTIVE model:
the /review skill's compose step wants a modelId, and the orchestrator resorts
to reading settings files — wrong under QWEN_HOME isolation and after /model
switches (measured: a report stamped with a model that never ran the review).
Publish QWEN_CODE_MODEL exactly the way QWEN_CODE_SESSION_ID is published: the
first Config claims the process-global slot, only the claiming instance
republishes (on refreshAuth and every model-change notification), and
getShellContextEnvVars passes it through, omitted when absent. The daemon
limitation is the session ID's own — later sessions read the first session's
model — and is documented at both the producer and the consumer.
* fix(core): clarify QWEN_CODE_MODEL daemon comment and cover refreshAuth republish (#7993)
* test(cli): cover stampCliEntryEnv wiring in runCliEntryPoint (#7993)
* fix(core): publish QWEN_CODE_MODEL per session and preserve entry mode on stamp (#7993)
Address review feedback:
- Key QWEN_CODE_MODEL on the session (registerSessionModel/getSessionModel),
mirroring the project dir, so daemon-mode subprocesses read their own
session's active model instead of the first session's. The process-global
slot remains as the single-session CLI fallback. This also neutralizes the
order-dependent claim: a throwaway Config's registration is keyed under a
session id no real spawn resolves.
- stampCliEntryEnv now adds exec bits to the existing mode (mode | 0o111)
rather than setting 0o755, so a private 0o600 checkout becomes execable
without becoming world-readable.
- Cross-reference scripts/dev.js and scripts/start.js in the stamp doc comment
and note the bundled `node dist/cli.js` launch is intentionally not stamped.
- Widen the AuthType test mock to include QWEN_OAUTH, pin the stamp-before-run
ordering in the wiring test, and cover the per-session model lookup.
* fix(cli): correct comment on Vite rewrite mechanism in protocol guard (#7993)
* fix(core): re-key per-session model registry on startNewSession (#7993)
startNewSession minted a new session id and re-stamped QWEN_CODE_SESSION_ID
but left the per-session model registry keyed on the outgoing id. After
/clear (or /reset, /new, /resume) a non-owner Config's subprocesses then
resolved the model by the new id, missed, and fell back to another
session's value. Unregister the old entry and republish under the new id.
Also correct the stampCliEntryEnv comments: npm start / npm run dev route
through scripts/start.js and scripts/dev.js, which stamp QWEN_CODE_CLI
themselves, so the only uncovered launcher is a direct node dist/index.js.
---------
Co-authored-by: verify <verify@local>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@alibaba-inc.com>
* feat(review): add `review run` — headless review with a machine-readable verdict
The review pipeline already runs non-interactively: `qwen --prompt "/review …"`
expands the bundled skill, launches the dimension agents, and honors the
approval mode. What that path lacks is a contract. The verdict lives in the
model's prose and in files whose names the caller must simply know, the exit
code says nothing about the outcome, and piped stdin silently defeats
slash-command detection (the runner prepends piped input, so the leading `/` is
no longer first). Anyone who wants "run a review, tell me what it decided" ends
up scraping a terminal.
`qwen review run [target]` is that contract and nothing more. It assembles the
/review invocation from typed flags (--effort, --comment), re-enters this
build's own CLI in a child process with stdin closed, streams the child's
progress to stderr, and then reads the verdict from the artifact compose-review
wrote — the same JSON the skill treats as the verdict authority — never from
anything the model printed. stdout carries only the result (human lines, or the
full JSON with --json).
Exit codes make the outcome scriptable without parsing: 0 = the review
completed (whatever it decided), 1 = it never reached a verdict (child failure,
timeout, or no composed artifact — a clean child exit without one is a run that
wandered off, not an approve), 3 = completed AND --fail-on request-changes AND
the event is REQUEST_CHANGES, so a CI gate can tell "blocking verdict" from
"the tool broke".
Artifact discovery is scoped to this run (mtime cutoff with a small slack for
coarse filesystem clocks): a stale composed JSON from an earlier review says
whatever THAT review decided, which is exactly the wrong thing to republish.
* fix(cli): harden review run against EPIPE, target injection, and drift (#7983)
- Use writeStderrLineSafe in the timeout and spawn-error handlers and guard
the progress stream, so an EPIPE on stderr can no longer skip the child
kill, hang the promise, or orphan the review.
- Reject a review target carrying whitespace or a leading dash before it is
re-tokenized by the child CLI (e.g. `123 --comment` silently authorising
posting).
- Constrain --approval-mode to the same choices as the top-level CLI.
- Capture the child's exit signal and surface it (OOM/SIGKILL vs spawn fail).
- Sync the top-level `qwen --help` review description with the command.
- Register `run` in the review.test.ts subcommand expectation and add tests
for the timeout branch, the readComposed guard, and target rejection.
* fix(cli): kill process group on review run timeout, harden edge cases (#7983)
The CLI relaunches itself in a child process (for --max-old-space-size),
so child.kill() only reached the relaunch wrapper — the real review was
reparented to PID 1 and kept burning API calls. Spawn with detached:true
and kill the process group (-pid) so the timeout actually terminates the
review.
Also: clamp negative --timeout-minutes to a 1-minute floor, distinguish
a corrupt composed artifact from a missing one in human-readable output,
and add test coverage for the default (non-JSON) output path.
* fix(cli): use specific MockInstance type for process.kill spy (#7983)
* fix(cli): capture review run verdict before cleanup, forward signals (#7983)
* fix(cli): reject quoted review targets, pin signal forwarding (#7983)
* fix(cli): keep captured review verdict when timeout fires after compose (#7983)
---------
Co-authored-by: verify <verify@local>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: qwen-code-bot <qwen-code-bot@users.noreply.github.com>
Address review feedback on the XML tool call fallback: stop corrupting
whitespace-sensitive arguments by stripping only delimiting newlines
instead of a blanket trim, veto recovery when the invoke example sits
inside a fenced code block so documentation is never executed, and keep
in-memory history aligned with the yielded synthetic chunk when the XML
spans multiple consolidated parts. Adds focused unit and integration
coverage, including a recording-alignment test.
* fix(core): preserve active Todo context across tool turns
* test(cli): update automatic turn prompt expectation
* fix(core): preserve Todo ownership across automatic turns
* fix(core): preserve Todo ownership at prompt boundaries
* test(todo): cover automatic reminder boundaries
* fix(core): throttle active Todo reminder re-injection to bound history growth
Every injected reminder copy lands permanently in chat history, so per-turn
injection grew the live context linearly with tool turns. Tool-turn injection
now re-issues the reminder only every third tool turn since the state was
last presented; turn-start injections always fire and reset the cadence. The
payload becomes a compact status/content line list capped at 800 characters.
History stays append-only, so provider prefix caching is unaffected.
Also: cover the new-ordinary-prompt-clears-stale-reminders invariant on the
real Config, add TUI coverage for the work-chain notification batch split,
cover todoWorkChainId continuation forwarding, and document the deliberate
enterWith binding in the daemon tool runner.
* fix(core): keep todo reminder before drained input
---------
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
estimatePromptTokens's steady-state branch uses char/4 to estimate the
size of newly-added content (e.g. tool results just appended to history).
char/4 is a flat ratio that does not account for CJK text density, and two
independent real production failures (both 2026-07-28, on
KAT-Coder-V2.5-Dev and Nex-N2-mini) traced back to this: large CJK-dense
tool results (parallel read_file calls returning design docs) were
under-counted by 39-54%, so clampOutputTokensToWindow computed a
maxOutputTokens that was too large, and prompt + max_tokens exceeded the
window by ~1 token in production, triggering a 400 that propagates past
reactive compression (which correctly detects it has no room left, but
can't undo an already-oversized request).
Add an opt-in `conservative` parameter to estimatePromptTokens that
inflates only the new-content term by 1.5x (ceil'd) — calibrated against
the two observed under-count ratios with headroom, applied only at the
two call sites that feed clampOutputTokensToWindow (where under-counting
is dangerous), left off at the cheap-gate/compaction-trigger call sites
(where the existing code comments already document under-counting as
safe — it only makes compaction trigger earlier, never skip it).
Co-authored-by: Zamba Lee <zamba@thinktop.com>
* feat(github-channel): add reasonFilter config to skip unwanted notification reasons
Adds an optional `reasonFilter` allowlist to the GitHub channel config.
When set, notifications whose `reason` is not in the list are skipped
before any lane dispatch, reducing unnecessary API calls and agent work
for notification types the operator does not care about.
- New `reasonFilter?: string[]` field on `GithubConfig`
- O(1) Set lookup (`reasonFilterSet`); undefined = no filter (all reasons)
- Early-skip in the poll loop, before subject URL extraction and lane dispatch
- Two tests: filtered reasons skipped, unset filter processes all
Default behavior is unchanged (undefined = process all reasons).
* fix(channels): log github reason filter skips
* fix(channels): validate github reason filter
* fix(channels): address github reason filter comments
* fix(github-channel): reject invalid reason filters
* fix(github-channel): validate reason filters on connect
---------
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
The transport stream retry was gated on !streamYieldedChunk, which any
thinking/reasoning chunk trips within seconds for thinking models. A
gateway closing the long-lived SSE connection minutes into the thinking
phase (UND_ERR_SOCKET) then propagated as a fatal error even though
zero user-visible content had been produced — making large generations
in YOLO/headless mode effectively impossible (#7832).
Track a separate streamYieldedContentChunk that is only set when a
chunk carries a non-thought candidate part (text, functionCall, …) and
gate the replay on it instead. Thought parts are ephemeral — never
recorded as the assistant's response in history — so replaying after
thinking-only output cannot duplicate visible content. Content-phase
failures keep the existing behavior: once a non-thought part has
flowed, the retry stays blocked and the error propagates.
The not-taken log now reports skipped_after_content, and the retry log
gains yieldedNonContentChunks so thinking-phase replays are visible in
diagnostics.
Fixes#7832
Co-authored-by: ComplexSimply <rudy.arrowsong@gmail.com>
* fix(core): add certified session writer handoff
Seal managed writer ownership after a durable recorder drain and allow trusted replacements to take over only when the transcript proof still matches.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(core): retain writer claim after failed rollback
Keep the fixed transition claim unless the exact predecessor primary is restored, and avoid rollback after claim ownership changes or unlink completes with a reported error.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(core): close session handoff claim races
Wait for claim-aware primary candidates to back out of transition gaps, and release losing claims when no primary transition started.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(core): bound session handoff candidate waits
Fail closed when a claim-aware primary candidate is abandoned during a handoff transition, preserving the transition fence for authoritative recovery.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(core): reconcile session handoff proof edges
Reject dangling transcript paths and reconcile ambiguous lock-record link outcomes by exact bytes before continuing a certified handoff.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(core): preserve inspect error cause and document handoff fail-closed paths (#7976)
---------
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
* feat(core): preload deferred tools within a context-window threshold
Adds tools.toolSearch.threshold (default 10, percent of the context
window). At session start, when the combined estimated schema footprint
of every deferred tool - bundled built-ins and MCP alike - fits within
the budget, all are revealed upfront so the declaration list stays
stable for the whole session and prefix KV caches survive; otherwise
everything stays deferred. Set 0 to always defer. Mirrors Claude Code's
ENABLE_TOOL_SEARCH=auto threshold mode, extended to bundled deferred
tools because here every reveal rewrites the declaration list and busts
the prompt-cache prefix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(core): log deferred-tool preload budget decision
Emit debugLogger diagnostics in preloadDeferredToolsWithinBudget covering
the computed budget, estimated token footprint, candidate count, and which
branch of the all-or-nothing gate was taken (no candidates, over budget, or
preloaded). Lets an operator diagnosing session-startup cost tell from debug
logs whether the deferred set fit the budget or was left behind ToolSearch,
without adding temporary instrumentation. No behavior change.
* fix(tools): bound toolSearch.threshold to 0-100%
The threshold setting is a percentage of the context window but had no
upper bound, so a value like 200 (a typo or misreading of the "(%)"
label) made the preload budget exceed the whole window and
unconditionally preloaded every deferred tool — the opposite of the
prefix-stability the threshold buys.
- Add minimum:0/maximum:100 to the setting schema (jsonSchemaOverride,
like autoCompactThreshold) and regenerate the VS Code settings schema.
- Add a symmetric runtime upper guard next to the existing
'thresholdPercent <= 0' lower guard in client.ts, clamping to 100% so a
hand-edited settings file cannot slip a larger budget past validation.
Adds a client test asserting a 200% threshold clamps to a full-context
budget.
* test(core): cover configured preload budget
* fix(tool-search): harden preload threshold
* test(tool-search): cover preload exclusions
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(core): rebind fork capabilities on resume
* refactor(core): share fork parent-tool extraction and cover resume guards
Extract the duplicated parent-tool-name derivation (flatten function
declarations, drop EXCLUDED_TOOLS_FOR_SUBAGENTS, dedupe) into a shared
extractParentToolNames() helper in agent-core, used by both fork launch
(the Agent tool) and fork resume (resolveCurrentForkRuntime), so the two
paths cannot silently diverge when the extraction or exclusion logic changes.
Add tests for the two previously-uncovered resolveCurrentForkRuntime
early-return branches: (1) every advertised parent tool is excluded from
subagents, and (2) no advertised tool is still registered in the live
ToolRegistry. Both keep the fork paused with the current-runtime-unavailable
blocked reason.
* test(core): cover fork-resume runtime guards and add resolve diagnostics
Address review feedback on the fork capability-rebind path:
- Add a direct unit test for extractParentToolNames (agent-core.test.ts)
covering cross-group flattening, dedup, EXCLUDED_TOOLS_FOR_SUBAGENTS
filtering, empty/undefined name filtering, and undefined/empty config.
- Add a fork-resume test asserting the MCP, skill, and deferred-tool
reminder branches of buildForkResumeCapabilityReminder are injected into
the resumed task prompt (previously only exercised with empty data).
- Emit debug diagnostics at each early return in resolveCurrentForkRuntime
(no_system_instruction / no_advertised_tools / no_registered_tools) so a
paused fork's blocking cause is distinguishable in logs.
* fix(core): address fork capability review
---------
Co-authored-by: Claude <noreply@anthropic.com>
Every gate compose-review enforces proves the agents READ the diff — coverage
proves the transcripts, anchors prove the quotes — but none proves the review
could tell good code from bad. Dogfooded: a weak-model run drafted nothing
from its entire roster on a diff where stronger same-condition runs found a
verified blocking Critical, and compose printed a bare, confident
"Verdict: Approve". A reader takes that as evidence of quality when it is only
absence of signal — the most dangerous shape a review verdict can have.
Disclose it, deterministically. When the composed event is APPROVE and the
plan's srcDiffLines — the same field the review topology is chosen from, with
tests, docs and generated files excluded by construction — exceeds 100, the
verdict line names the shape:
Verdict: Approve — low signal: none of the 11 review agents reported a
finding on a non-trivial diff (155 source diff lines)
The event never moves on it: a cap would punish every genuinely clean diff,
and nothing was in fact found. Docs-only and typo-class diffs keep their bare
Approve — there, finding nothing is the expected outcome. The floor sits well
past the typo-fix class (a tiny edit scattered one line per hunk stays under
it) and at a fifth of the smallest diff the topology gate calls big. Both
numbers in the line are the run's own: the roster the plan required (all on
record at APPROVE, or coverage would have capped) and the plan's source-line
count.
Co-authored-by: verify <verify@local>
* fix(cli): stop resize repaint from causing scroll storm (#8004)
Remove the useResizeSettleRepaint -> refreshStatic wiring that wrote
clearTerminal (destroying scrollback) and remounted <Static> on every
settled resize, re-emitting all conversation history in 50-item chunks.
Ghostty's panel-toggle animation exceeds the 200ms debounce, triggering
multiple settle-repaint cycles per toggle -- visible as continuous
scrolling/flickering.
Ink's dynamic region already re-renders on width changes via
useTerminalSize; modern terminals handle scrollback reflow natively.
The full remount is no longer necessary. The now-unused hook and its
test are removed (no remaining callers).
* test(cli): guard resize no-repaint contract against settle-time regression (#8004)
* test(cli): make resize settle regression test non-vacuous (#8004)
The previous test used rerender() which remounts the tree via ink's
ErrorBoundary (measureElement returns undefined → layout effect throws
→ tree unmounted), so the settle debounce never fired and the test
passed regardless. Rewrite to keep the tree alive (measureElement mock
returns a real value) and deliver width changes to the same mounted
instance via a listener pattern. Mutation-verified: fails when the
removed useResizeSettleRepaint hook is restored.
* chore(cli): remove dead useResizeSettleRepaint from eslint legacy filenames (#8004)
---------
Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Anthropic's Messages API rejects any tool `input_schema` carrying a
top-level `oneOf`/`allOf`/`anyOf` combinator with a hard 400:
input_schema does not support oneOf, allOf, or anyOf at the top level
send_message's schema used `oneOf: [{ required: ['to'] }, { required:
['task_id'] }]` to express "either a teammate recipient or a
background-task id is required." Since this schema is forwarded
verbatim as `tools[].input_schema` to Anthropic
(anthropicContentGenerator/converter.ts -> convertSchema), every call
to send_message on an Anthropic-backed model (native API, Vertex, or
an Anthropic-compatible proxy) failed outright -- not degraded, fully
unusable.
Verified live against the Anthropic Messages API (claude-sonnet-5):
the schema with `oneOf` 400s with the exact message above; the
identical request with `oneOf` removed succeeds (200, tool_use
returned).
The `oneOf` constraint was redundant defense-in-depth, not the only
enforcement: send_message's execute() already returns a clear runtime
error ("No active team and no task_id provided...") when neither `to`
nor `task_id` is supplied. Dropping the schema-level constraint leaves
that runtime validation intact and unchanged for OpenAI/Gemini-backed
models, while making the tool actually callable on Anthropic-backed
models.
Fixes#7984
Co-authored-by: Palanisamy, Dinesh <Dinesh.Palanisamy@netapp.com>
- Remove duplicate warn logging from tryRecoverXmlToolCalls (caller owns it)
- Add integration test for XML fallback in processStreamResponse
- Add test coverage for malformed JSON catch branch in parseParameterValue
- Remove now-unnecessary debugLogger mock from unit tests
* feat(channels): add pairing approval management API
* fix(sdk): expose pairing approval types
Re-export the new approval and revocation types from the public SDK entry, and pin the qualified workspace DELETE request body in regression coverage.
---------
Co-authored-by: qwen-code-dev-bot <269191875+qwen-code-dev-bot@users.noreply.github.com>
* feat(channels): add GitLab polling channel adapter
Poll GitLab todos via @gitbeaker/rest, dispatch notes through the
existing PollingChannelBase pipeline. Key design points:
- action_prompt_template config drives event filtering and metadata
rendering (unconfigured actions are skipped)
- Per-repo cursor (repo[chatId].last_read) as notes window lower bound,
global lastProcessedAt for todo-level dedup
- mark_done after successful processing; failure skips mark_done for
retry on next poll
- Mention gating delegated to base GroupGate (adapter only sets
isMentioned flag)
- First-contact body fallback for todos with no notes (e.g. mention in
issue description)
* fix(channels/gitlab): persist cursor after each successful todo
Call saveCursor() immediately after advancing lastProcessedAt so that
progress is durable even if the process crashes mid-poll. Also removes
the local watermark variable in favor of direct assignment.
* fix(channels/gitlab): persist cursor on every advancement including skips
* fix(channels/gitlab): address review critical issues
- Remove non-functional proxyAgent (gitbeaker doesn't support it)
- Construct repo_url from host + path (API doesn't return web_url)
- Handle directly_addressed action (falls back to mentioned template)
- First-contact fetches target description instead of using todo.body
- Move todo.project dereference inside try block
- Filter confidential notes
- Update channel-registry.test.ts for gitlab entry
* fix(channels/gitlab): address review suggestions
- Warn on connect if action_prompt_template is not configured
- Guard todo.target.iid before use
- Skip paths now mark_done (best-effort) to clean GitLab UI
- Remove postErrorComment (avoids duplicate comments on retry)
- Fetch only first page of notes (desc, maxPages:1, perPage:100)
instead of paginating entire note history
- Extract fetchRecentNotes for single-page windowed enumeration
* refactor(channels/gitlab): simplify to todo.body dispatch, add description mention support
- Remove notes API fetching; dispatch todo.body directly
- Detect description mentions via target_url anchor (#note_ absence)
- Always fetch target description for %description% metadata
- Remove per-repo cursor; dedup via cursor + mark_done only
- Cursor advances regardless of success/failure (no retry)
- Use zod for cursor validation
- Rename template vars to GitLab terminology:
%project% %project_url% %target_type% %iid% %title% %description% %todo_id%
- Support %% escape for literal percent
* docs(channels): add GitLab adapter documentation
- New user guide: docs/users/features/channels/gitlab.md
- Update _meta.ts navigation
- Update developer adapter matrix and SDK list
* fix(channels/gitlab): use correct Issues.show(issueIid, { projectId }) signature
* chore: regenerate NOTICES.txt for new gitlab channel dependencies
* fix(channels/gitlab): address review suggestions
- Add todo.project null guard (item 2)
- Single-pass regex for %% escape + %var% substitution (item 4)
- sendThreadMessage throws directly on undefined threadId (item 5)
- Dedup fetchDescription with per-poll cache (item 6)
- Remove per-todo saveCursor; base class saves after pollOnce (item 7)
- Add undefined threadId test (item 8)
- Expand confidential notes limitation in docs (item 3)
* test(channels/gitlab): add mention tests, directly_addressed coverage, skip assertions, temp cleanup
- New mention.test.ts: 14 cases for testBotMention/stripBotMention/escapeRegex
- Add directly_addressed fallback test
- Skip tests now assert TodoLists.done + cursor advancement
- afterEach cleans up mkdtempSync temp dirs
* fix(channels/gitlab): address review round 4
- Non-mention actions (assigned, etc.) set forceMentioned=true to bypass GroupGate
- Merge dead note-filter tests into single 'skips todo authored by bot'
- Log fetchDescription errors to stderr instead of silent swallow
- Post error comment on issue/MR when handleInbound fails (best-effort)
* fix(channels/gitlab): always force isMentioned=true, remove regex re-derivation
The action_prompt_template config is already the event filter, and
GitLab has already decided the mention when creating the todo.
Re-deriving isMentioned via regex on todo.body causes permanent
message loss when the regex misses (description mention + fetch
failure, group mentions). Always set forceMentioned=true so
GroupGate never drops a todo that passed the template filter.
* fix(channels/gitlab): propagate fetchDescription errors for description mentions
For note mentions, description is metadata-only — fetch failure is
logged and swallowed. For description mentions, description IS the
message — fetch failure now propagates to the outer catch, which
posts the ⚠️ error comment so the user knows to re-mention.
* perf(channels/gitlab): clean up stale todos, skip unnecessary fetchDescription
- Mark stale todos (updated_at <= cursor) as done on each poll to
prevent perpetual re-fetching of pre-existing pending todos
- Skip fetchDescription for note mentions when template does not
contain %description%, saving one API call per todo
- Update docs: stale todo cleanup, error comment on failure
* docs(channels/gitlab): clarify requireMention is bypassed, template is the real filter
* Apply suggestions from code review
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
* fix(channels/gitlab): use todo ID cursor instead of timestamp to eliminate equal-timestamp loss
Timestamp-based cursors (second granularity) could silently destroy
todos sharing the same updated_at as the cursor boundary. Switch to
monotonically increasing todo IDs which are unique and collision-free.
Add initialized flag to preserve first-start drain semantics: pre-existing
pending todos are marked done without dispatch on the first poll cycle.
* fix(channels/gitlab): harden first-poll drain, add ordering tests, fix lockfile
- Replace Math.max(...spread) with reduce to avoid RangeError on large
backlogs (~100k+ todos). Move initialized=true after the drain work so
any throw retries the drain instead of falling through to dispatch.
- Add unit tests: identical-timestamp delivery and id-order-when-updated_at-disagrees
(kills M2 sort mutant).
- Align lockfile: file:../base → ^0.21.0 for channel-base dep.
* fix(channels/gitlab): include dot in mention lookahead for GitLab usernames
GitLab usernames may contain dots (e.g. bot.name). The lookahead
character class inherited from GitHub omitted '.', causing @bot.name
to match as @bot. Add '.' to the negated class.
* docs(channels/gitlab): align docs with ID cursor and drain semantics
- Add first-poll drain as step 2 in How It Works
- Clarify GroupGate always passes (isMentioned forced true)
- Document initialized flag in Known Limitations
* Apply suggestions from code review
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
* fix(channels/gitlab): align package version and channel-base dependency to 0.21.1
Bump version from 0.21.0 to 0.21.1 to match other channel packages after
upstream merge. Pin @qwen-code/channel-base to exact 0.21.1 instead of
^0.21.0, matching the convention used by other published channels.
* fix(channels/gitlab): regenerate lockfile to match package.json versions
Manually add only gitlab-related lockfile entries (workspace, @gitbeaker
packages, transitive deps, channel-gitlab link) without unrelated npm
normalization churn.
* test(channels/gitlab): add regression tests for first-poll drain hardening
Two tests that kill the M1 (Math.max spread RangeError) and M2 (flag
ordering) mutants which survived the original 46-test suite:
- 150k todo drain verifies reduce() handles large backlogs without
RangeError and without dispatching
- Drain throw verifies initialized stays false so the next poll retries
the drain instead of falling through to dispatch
Test file duration: ~40ms → ~170ms.
* docs(channels/gitlab): clarify groupPolicy must be "open" and add runtime warning
The default groupPolicy "disabled" silently drops all mentions — todos are
marked done and cursor advances, but no dispatch occurs. Fix misleading docs
that said "GroupGate always passes" (only true at groupPolicy: "open") and
add a connect()-time warning when groupPolicy is not "open".
* fix(channels/gitlab): correct xcase integrity hash in lockfile
The manually added xcase entry had a typo in the sha512 hash (ys → ks),
causing npm ci EINTEGRITY failures in CI.
* fix(channels/gitlab): correct requester-utils integrity hash in lockfile
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(channels/gitlab): allow groupPolicy "allowlist" in warning and docs
The groupPolicy warning and docs incorrectly stated that groupPolicy
must be "open". In reality "allowlist" with the project listed also
works because isMentioned is forced true and GroupGate only requires
the group to be listed. Also fix the inaccurate "no error is logged"
claim — ChannelBase logs preflight rejected reason=group_disabled.
Fixes R5-🟡3 from PR #7862 review.
---------
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
- Yield a synthetic chunk with recovered functionCall parts so the
agent loop (turn.ts) actually executes them instead of silently
dropping them
- Recompute contentText/contentParts after the history splice so the
JSONL recording stays aligned with in-memory history (--resume)
- Guard recovery with streamError === null to avoid persisting
orphaned partial turns on mid-stream errors
- Add intent guard: only fire when XML blocks dominate the content,
preventing false positives on documentation or echoed XML
- Parse only structured JSON values (objects/arrays); preserve scalar
strings to avoid type coercion (e.g. file named "null")
- Use Object.create(null) for args to prevent __proto__ pollution
- Strip only parameterized invoke blocks from remainingText,
preserving parameterless blocks as plain text
* feat(web-shell): add split pane header action slot with overflow
Let hosts render per-session actions in each split pane header, collapsing them into a … menu when the pane is too narrow.
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs(web-shell): add pane header actions PR screenshots
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web-shell): tighten pane header overflow measurement
Drop the per-render children effect dependency that rebuilt ResizeObserver during streaming, and reserve workspace-tag width when computing available header space.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web-shell): address pane header overflow review blockers
Mount host actions in only one tree, and wrap overflow entries as DropdownMenuItems so Radix selection and keyboard navigation work.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web-shell): keep pane header actions alive across overflow
Flatten Fragment host actions before building the overflow menu, and keep the same host instances mounted when collapsing so stateful actions are not reset.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web-shell): address pane header overflow review suggestions (#7808)
* fix(web-shell): proxy overflow clicks via action slots
Wrap host pane actions in stable slots so the overflow menu can activate interactive descendants without requiring opaque custom components to forward internal data attributes.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web-shell): address overflow menu review suggestions (#7808)
* fix(web-shell): harden pane header overflow actions (#7808)
Restore the 8px gap between the built-in maximize/close controls, ignore
aria-hidden glyphs when labelling overflow items, omit non-interactive
children from the overflow menu, and document the popover constraint on
renderHeaderActions. Refreshes the design doc to match the mount-once
implementation.
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
* feat(hooks): add security.allowPrivateNetworkHooks to bypass SSRF range checks for trusted scopes
HTTP hooks hard-block all private/link-local address ranges via ssrfGuard,
which makes them unusable in platform-managed environments where the hook
receiver is a first-party, VPC-internal endpoint (e.g. an internal API
gateway resolving to 172.16.0.0/12).
Add an opt-in setting, security.allowPrivateNetworkHooks (default false),
that skips the SSRF IP-range checks in urlValidator.isBlocked (literal IPs)
and validateResolvedHost (literal + post-DNS-resolution paths).
Security properties:
- Honored only from User/System/SystemDefaults scopes; the value is
stripped from Workspace settings during the merge (with a startup
warning), so a cloned repository can never self-grant the bypass.
- BLOCKED_HOSTS (169.254.169.254, metadata.google.internal, ...) remains
blocked even when the flag is on.
- Default false keeps every code path byte-for-byte compatible with
current behavior; bare/safe mode forces it off.
* fix(hooks): enforce metadata endpoint blocklist regardless of allowPrivateNetworkHooks
Address review findings on #7968: with the flag on, cloud metadata
endpoints were reachable through gaps in the relaxed checks.
- ssrfGuard: add METADATA_IPS (169.254.169.254, 100.100.100.200) and
isMetadataAddress(), which normalizes IPv4-mapped IPv6 forms
(::ffff:a9fe:a9fe, ::ffff:6464:64c8, ...) via the existing
extractMappedIPv4/expandIPv6Groups helpers.
- urlValidator.isBlocked: BLOCKED_HOSTS matching and the literal-IP
isMetadataAddress check now run unconditionally; only the general
range check (isBlockedAddress) is relaxed by the flag.
- httpHookRunner.validateResolvedHost: no longer returns early with the
flag on — DNS resolution still runs and resolved addresses are checked
against isMetadataAddress, so a hostname resolving to a metadata
endpoint is blocked. DNS failures still defer to fetch, as before.
- settings warning text now lists User/System/SystemDefaults, matching
the schema and docs.
- docs: precise wording — the flag relaxes only range checks; metadata
endpoints stay blocked in all serialized forms and after DNS resolution.
The flag now opens RFC1918/CGNAT/link-local ranges only; cloud metadata
endpoints (169.254.169.254, 100.100.100.200 in any form, plus the
BLOCKED_HOSTS hostnames) are unreachable in every configuration.
---------
Co-authored-by: 欢伯 <ri.xur@alibaba-inc.com>
* fix(cli): MCP prompt completion no longer blocks Enter for optional params (#7991)
The completion handler treated all unused arguments (required + optional)
as completion suggestions with auto-appended `="`, making optional params
look mandatory. After selecting a prompt name, pressing Enter would
auto-append `--input="` instead of executing the prompt.
Two changes:
1. Completion handler now parses named args directly instead of using
parseArgs() (which returns Error on missing required args — the normal
state during tab completion).
2. Only required unused args are suggested as completions. When all
remaining unused args are optional, the completion list is empty so
Enter executes the prompt with defaults.
* fix(cli): restore optional and positional MCP prompt completion discovery (#7991)
* fix(cli): share named-arg regex and cover mixed MCP prompt completion (#7991)
* fix(cli): suggest optional args mid-keystroke and test multi-word positionals (#7995)
---------
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: qwen-code-bot <qwen-code-bot@users.noreply.github.com>