Commit graph

611 commits

Author SHA1 Message Date
rockybot2026
732f4d8a29
feat(voice): support trusted private ASR base URLs (#8350)
* feat(voice): support trusted private ASR base URLs

* fix(voice): address private endpoint review findings

* test(voice): cover private endpoint edge cases

* test(voice): pin remaining endpoint edge cases

* fix(voice): address private endpoint review feedback

* fix(voice): clarify allowlist URL and normalize IPv6

* fix(voice): harden NAT64 address validation

* fix(voice): address managed endpoint review findings

* refactor(voice): extract shared IPv6 transition unwrap ladder (#8350)

Deduplicate the IPv6-transition unwrapping sequence (mapped, compatible,
NAT64, dotted-quad) that was repeated verbatim between isPrivateNetworkIp
and isAlwaysBlockedVoiceAddress on both CLI and Desktop surfaces. A single
unwrapIpv6TransitionStep helper now yields the next canonical address (or
'blocked' for unrecognized ::ffff: forms), and each predicate recurses
through it, preserving the exact re-check semantics at every unwrap level.

* test(voice): cover allowInsecureBaseUrl wiring through desktop default transports (#8350)

* fix(voice): add allowlist hint to private-network rejection error (#8350)

* fix(voice): reject always-blocked base URLs before offering the allowlist hint (#8350)

* fix(voice): resolve exact desktop voice provider before OAuth (#8350)

* fix(voice): address review feedback for trusted private base URLs (#8350)

* fix(voice): align desktop voice resolution with CLI semantics (#8350)

* fix(voice): scope desktop fail-closed resolution to policy-bearing entries (#8350)

* fix(voice): address round-8 review findings for trusted private base URLs (#8350)

Run the invasive process-global `mock.module('ws')` suite as
voice-ws-handler.isolated.ts so the desktop package's single-process
`bun test` run no longer leaks the fake socket into unrelated ws
consumers; the existing isolated loop runs it in its own process.

Shape-guard the desktop provider scan: non-object modelProviders
elements are skipped (falling through to OAuth instead of throwing a
raw TypeError), and non-string baseUrl/envKey/settings.env values on a
voice-model entry now surface the PROVIDER_ENTRY_REMEDY remediation
error instead of crashing.

Compute the DashScope-compatible /v1 rewrite before any allowlist
match in fromExactModelProvider so the stage-1 check, the remediation
messages, and the top-level recheck all compare the same final URL and
a single allowlist entry converges for split-horizon deployments.

Extend the CLI allowlist remediation messages to state which settings
scopes honor the entry, since serve mode never shows the interactive
workspace-strip warning. Thread providerProtocol through the CLI voice
model seams (createVoiceModelSource and the daemon buildModelsConfig)
so protocol-mapped custom provider groups resolve like the rest of the
CLI model surface, and document the remaining protocol-agnostic desktop
scan in the design doc. Correct the getHomeEnvFallback comment: it
adopts the narrower getHomeEnvFallbackVars candidate set on purpose.

Add multi-record DNS answer tests on both CLI and desktop net guards so
the records.some classification is pinned against the array shape
defaultLookupHost always produces in production.

* fix(voice): address round-9 review findings for trusted private base URLs (#8350)

* fix(voice): address round-10 review findings for trusted private base URLs (#8350)

* fix(voice): classify desktop voice duplicates before ambiguity check (#8350)

* fix(scripts): compare voice guard mirrors as parse trees (#8350)

---------

Co-authored-by: rockybot2026 <265985139+rockybot2026@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-06 14:04:57 +00:00
Shaojin Wen
f4cf4268f5
fix(core): cap a streaming response's total lifetime, slim the review fan-out launch (#8602)
* fix(core): cap a streaming response's total lifetime

The stream inactivity watchdog resets on every chunk, so a drip-fed
stream — a gateway trickling keep-alive chunks, or a model crawling
through an oversized single message — kept it alive forever while the
message never completed: in issue #8597's CI review runs that meant
2.5-4.5 hours of silence ending in the outer timeout's kill.

Add a per-request lifetime cap (default 15 min) that does not reset on
chunk arrival. Tripping it throws a retryable ETIMEDOUT, so the
existing transport-continuation recovery resumes a healthy generation
the cap happened to cut. Config field streamMaxLifetimeMs; env knob
QWEN_STREAM_MAX_LIFETIME_MS; 0 disables.

* fix(review): carry findings lists as digest-named files, not inline

agent-prompt folded the cumulative findings list into every printed
verify/reverse-audit block. On a 12-14-auditor round that made the
launch one 65-82 KB assistant message — the oversized single message
whose stream generation never completed in #8597 — and cost a 5-10
minute paged relay through the orchestrator's context before every
round.

The list now goes where the brief already goes: on disk, named by the
same findings digest that keys the record; the block carries a read_file
pointer. The delivery guarantee is unchanged — a launch that drops the
read matches no record — and the retirement scheduler's echo guard
reads the list back from the file the prompt names, failing toward
auditing when it is gone.

* fix: address review feedback on stream guards and findings files

* fix: address review feedback on findings delivery and stream guards

The delivery floor counted only the brief's read receipt, so an
instruction-skipping verifier could open its brief, skip the one
instructed read of the findings file its block points at, and clear
the gate having never seen the list it ruled on. The floor now
extracts the findings pointer from the recorded prompt and requires a
successful read of it — a new findings-unread delivery shape, with gap
texts for the verify, reverse-audit, and combined steps. A failed
findings-file write also speaks on stderr instead of silently pointing
a whole round at a missing file.

Stream guards: consult the lifetime deadline at the top of the loop,
not only when the timer wins the race — a pre-buffered chunk resolves
next() as a microtask and beats setTimeout(…, 0) every time. Merge the
two ETIMEDOUT bypass blocks into one rule. New tests pin the
idle-off + lifetime-on branch, the both-guards-0 disable (config and
env), and the buffered-source shape.

Also reword the prose sites that still described the inlined list.

* fix(review): name the findings file in the verify/reverse-audit briefs

The two briefs still told the agent the findings were "listed in the
message that launched you", but since the list moved behind a
read_file pointer they find only a pointer there. The agent reads its
brief first (the block mandates it), so the stale text sent it looking
for finding blocks in the launch message — the exact findings-unread
shape the new delivery floor gates on. Point both briefs at the
.findings.md file and the required read instead.

* fix(review): address round-3 review — empty-round brief, guard hardening

The b796708 brief fix stated unconditionally that the launch message
points at a findings file, but an early reverse-audit round on a clean
review names no file (the empty branch prints '## Nothing is confirmed
yet' with no pointer) — the auditors were being sent to read a path
their prompt does not contain. The reverse-audit brief now says the
file is named when there is a list, and that an early round has none.

Also from the round-3 review:
- writeFindingsFile's failure diagnostic now uses writeStderrLineSafe:
  the catch exists to keep the build alive, so it must not throw out of
  it on EPIPE (qwen … | head).
- The retirement echo-guard resolves the findings pointer from the
  CLI's own record (never the orchestrator's pasted copy), confined to
  the plan's record dir, memoized per round — an out-of-bounds or
  unreadable file degrades to the prompt, failing toward auditing.
- Document the delivery floor's deliberate weakening: it proves the
  findings file was opened, not paged to completion (coverage.ts), and
  drop the last stale 'record folds the findings in' comment.
- withStreamInactivityTimeout returns the source untouched when both
  guards are disabled, so the invariant survives a caller refactor
  (setTimeout(Infinity) would otherwise clamp to ~1ms).
- settings.md + the config comment now say the stream guards are
  OpenAI-compatible-only, and that the 15-minute cap bounds a stream
  whose idle timeout was raised above it (raise or disable the cap to
  keep a longer window).

* fix(review): address round-4 review — memo hygiene, read-visibility, naming

The retirement echo-guard memoized a failed read's per-record fallback
under the round's shared findings pointer, so one chunk's launch prompt
would serve as a sibling chunk's findings list. Memoize only successful
reads; a miss falls back to THIS record's own prompt, uncached.

The findings block now prints the list's line count, so an agent whose
read_file truncates can see it saw a fraction rather than the whole
confirmed list — a visibility aid the delivery floor (which proves the
file was opened, not paged) does not provide.

Also from the round-4 review: rename withStreamInactivityTimeout to
withStreamGuards (it now enforces two guards), correct the constants
comment (the cap is measured from the stream's first iteration, not
its first byte), and give the two stream-guard knobs a dedicated
settings.md entry instead of burying them in the timeout paragraph.

* fix(review): address round-5 review — deterministic record walk, accurate line count

The cross-contamination regression test only discriminated under one
readdir order: readRecordedPrompts walked the record dir in filesystem
(filename-hash) order, so which chunk's fallback poisoned the shared
memo depended on the walk. Sort the directory listing — a deterministic
walk removes that whole class of order-sensitivity from a module that
reasons per-record, not just from this test (a temporary revert of the
memo fix confirms the test now fails against the buggy shape).

findingsSection printed the line count of the TRIMMED findings body
while writeFindingsFile writes the untrimmed content, so a list with
leading/trailing blank lines got a label smaller than the file the
agent actually reads — precisely the under-reading the count exists to
make visible. Count the untrimmed content.

* fix(core,review): charge the lifetime cap on upstream-wait, not delivery time

Round-2's top-of-loop deadline check made the lifetime cap measure
end-to-end delivery time: a healthy upstream that finished and buffered
its chunks was cut for the CONSUMER's slowness (a paused IDE client, a
big render), and a stream whose terminal done resolved at the boundary
was converted into a retry. The cap is now charged on accumulated
upstream-wait — the time the loop is blocked in await it.next() — so a
buffered, already-complete stream always completes and only real
upstream latency counts; the drip-fed never-completing stream spends
exactly that time waiting, so #8597's shape is still caught.

Also from the round-6 review:
- Hoist the stream-guard error branch above the thinking-tag check: a
  drip-fed gateway cutting mid-<think> surfaced the guard's ETIMEDOUT
  as a PROTOCOL_TAG_LEAK and burned the tag-leak retry budget.
- The findings line count drops the trailing newline's empty segment,
  so a 12-line list is not advertised as 13.
- settings.md: the stream guards are env/config-only (no settings.json
  key); document that streamIdleTimeoutMs: 0 embedders now also need
  streamMaxLifetimeMs: 0 to fully opt out.
- constants: a functionCall already streamed (the tool-heavy common
  case) recovers as a visible classified error, not a continuation.
- coverage.ts: drop the stale 'four shapes' counts after Delivery grew
  a fifth; pipeline: dedupe the instanceof in the guard debug log; and
  correct the both-guards-off test's comment to pin the outcome, not
  the caller mechanism.

* fix(core,review): monotonic guard clock, findings-write fallback, read-only floor

Round-6 follow-ups the previous commit left open:

- The stream guards accounted on Date.now(): an NTP step forward (or a
  long sleep) killed a healthy generation on the next iteration, and a
  backward step silently disabled the lifetime cap — the hang #8597
  exists to bound. All guard accounting is now performance.now(); the
  setTimeout it races is monotonic too, so the two agree. Vitest fakes
  performance alongside the timers, and a new test A/B-verified against
  the wall-clock shape (it fails on Date.now() accounting).

- A failed findings-file write returned the path anyway, pointing a
  whole 12-14-agent round at a file that does not exist — every agent
  burned its round, then the delivery floor failed it. writeFindingsFile
  now returns null on failure and findingsSection falls back to inlining
  the list (the pre-#8597 shape): the recorded prompt carries the list,
  the delivery check compares it verbatim, and the build stays alive.

- The findings delivery floor matched the path in ANY successful tool
  call's serialized args, so a search_file_content or list_directory
  that merely named the file cleared it without reading a line. The
  transcript parser now records read_file calls apart, and the floor
  counts only those — a mention is not an open.

* fix(core,review): round-7 leftovers — upstream-wait wording, key helper, test mock

The round-6 error message and class doc still said 'total lifetime cap'
and promised the continuation recovery unconditionally, and the
wrap-site comment still read 'aborts at maxLifetimeMs from stream
start' — all three now describe the upstream-wait semantics the guard
actually implements, and the message names the wall clock separately so
the two numbers reconcile.

The round suffix baked into findings-role record keys and the findings
file name was derived independently in three sites (findingsFileFor,
runAllChunks, the single build) — a change to how a round is spelled
would update two of three and silently fork the artifacts; roundPartOf
spells it once.

The writeFindingsFile test's module mock replaced all of stdioHelpers
(three stubs, missing the other exports) and asserted mock.calls[0][0]
with no mock reset — it now spreads importOriginal, clears mocks in
beforeEach, and matches the stderr lines with stringContaining.

* fix(review,core): round-3 follow-ups — conditional findings briefs, JSDoc placement, log labels

The verify/reverse-audit briefs told the agent its findings live in the
.findings.md file unconditionally, on the exact path (writeFindingsFile
returning null -> findingsSection inlining the list) this PR added to
work WITHOUT one; both now say 'when the message points at a findings
file, read it; on the rare write-failure fallback the list is inlined
in the message, read it there.'

roundPartOf had been inserted between findingsFileFor's JSDoc and the
function, orphaning the comment that records the one-file-per-round
and null-means-inline contracts; the helper now sits above it.

The guard debug log labelled the wall clock 'streamLifetimeMs' beside
the cap; it is now 'wallClockMs' so the two numbers reconcile the same
way the error message does.

* fix(review): anchor the findings-pointer extraction to its emitted shape

findingsPointerOf matched the FIRST read_file(file_path="….findings.md")
anywhere in the recorded prompt. On the write-failure inline fallback the
findings list occupies exactly the position the pointer would sit in, and a
finding entry there can itself quote a read_file pointer of its own (a
finding about this pipeline, which the harness produces when it reviews this
repo). The loose match then extracted the quotation as the pointer and the
readers diverged: coverage demanded a read of a path no agent was told to
read (a spurious findings-unread on a run that is already degraded), and
retirement, worse, confined-and-read an earlier round's file and flipped a
just-filed finding to an echo, retiring a chunk that had just reported — the
one direction the module's header commits to never failing. A quoted pointer
inside a findings entry is indented or embedded in prose, so anchoring to a
standalone read_file line removes it; the happy-path pointer is alone on its
own line inside its fence. A test drives an inlined list containing a
pointer-shaped line (fails under the old loose regex).

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-06 13:27:30 +00:00
Shaojin Wen
477859bb3f
feat(channels): support local gh authentication (#8461)
* feat(channels): support local gh authentication

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

* test(channels): align registry catalog test and visuals with optional GitHub token (#8461)

* fix(channels): address review feedback for GitHub local gh auth (#8461)

Treat a blank replacement of an optional secret as a clear so an existing GitHub channel can no longer ship an empty or whitespace-only PAT to the daemon. Reuse the shared missing-field predicate in the editor's GitHub credential validation, wrap malformed baseUrl failures in an actionable channel error, and surface sanitized gh stderr in local authentication failures.

* fix(channels): address second-round review feedback for GitHub local gh auth (#8461)

Pin the whitespace-only token gate, the bounded gh stderr sanitization, and the required-secret blank-replacement guard with mutation-resistant tests. Log the authenticated account identity on channel connect so an out-of-band gh auth switch is visible to operators. Align test secret-source fixtures with the SDK union and complete the design doc's change footprint.

* fix(channels): address third-round review feedback for GitHub local gh auth (#8461)

* fix(channels): address fourth-round review feedback for GitHub local gh auth (#8461)

* fix(channels): address fifth-round review feedback for GitHub local gh auth (#8461)

* fix(channels): address sixth-round review feedback for GitHub local gh auth (#8461)

* fix(channels): address seventh-round review feedback for GitHub local gh auth (#8461)

* fix(channels): address eighth-round review feedback for GitHub local gh auth (#8461)

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-05 14:05:00 +00:00
Shaojin Wen
a123d0030a
ci(review): prepare evidence-image tooling for GitHub-triggered reviews (#8454)
* ci(review): prepare evidence-image tooling for GitHub-triggered reviews

Reviews triggered on GitHub cannot attach images today for three
reasons; this wires the two that live in the workflow:

- Install tmux and freeze (pinned, checksum-verified) before the
  review runs. Both are optional by contract — the evidence ladder
  degrades honestly without them (png -> ans-only -> refused, recorded
  in the capture manifest) — so the step never fails the review; it
  only decides which rung the runner can reach. tmux mirrors the
  tolerant install qwen-autofix.yml already uses; freeze falls back to
  ~/.local/bin when passwordless sudo is absent.

- Pass QWEN_REVIEW_ASSETS_REPO from a repository variable to the
  review step. Publishing stays OPT-IN by design: with the variable
  unset the env is empty and publish-assets refuses (parseAssetsRepo
  trims and rejects empty), so nothing changes until a maintainer sets
  the variable. When set, evidence images land on commit-pinned
  pr-assets/<pr>-review branches — already covered by the visuals
  cleanup workflow — pushed with the same CI_BOT_PAT the step uses.

The third reason is release lag: the capture producer (capture-tui,
#8388) has to merge and ship in a release before rendering claims can
generate images on CI at all. This change is inert until then.

* fix(ci): capture-tools step review fixes — enforced tolerance, version pin, cached fallback

R1-1: the never-fails contract is now enforced twice — continue-on-error
at the YAML level (the belt) and set +e with a trailing exit 0 inside
(the suspenders); under the runner's default bash -e several statements
(mktemp, install, sudo install with an empty path) could previously
abort the step and fail the review the comment promised never to fail.

R1-6: probe the VERSION, not just the binary — on a persistent
self-hosted runner an installed freeze made any FREEZE_VERSION/SHA
bump a silent no-op; the pin now forces a refresh when the cached
binary does not match.

Cached-fallback fix: put ~/.local/bin on PATH (and GITHUB_PATH) before
the probe — a sudo-less runner otherwise re-downloads the tarball on
every review run forever.

R1-3: the step comment says capture-tui is UPCOMING (#8388, not in the
released CLI) and names qwen review drive as today's tmux consumer, so
the step cannot be mistaken for stale dead weight and deleted from
under the follow-up.

R1-4: the retention comment scopes the cleanup-workflow claim to the
same-repository designation; a fork or scratch destination manages its
own retention (docs updated to match, plus a note documenting the
repository VARIABLE a maintainer sets to enable publishing).

R1-5: the step's real bash now runs in the workflow behavioural
harness under bash -e with stubbed sudo/apt/curl/sha256sum/tar/uname:
worst-runner and checksum-reject scenarios exit 0 installing nothing,
the no-sudo happy path pins the ~/.local/bin + GITHUB_PATH pairing,
and the version-pin probe is pinned from both sides (wrong version
re-downloads, matching version skips). Real freeze/sudo on a
developer machine are shadowed so the tests are deterministic and can
never install to /usr/local/bin.

Nit: both sudo guards now check sudo -n true.

* fix(ci): capture-tools step review fixes — step-owned tool dir, anchored probe, honest failures

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

* fix(ci): capture-tools test harness — shadow tmux, don't blank its PATH dir

The harness dropped every host PATH directory that ships a tmux so the
step's apt branch would depend on the scenario, not on the machine
hosting the suite. On GitHub-hosted ubuntu runners tmux lives in
/usr/bin, so the filter blanked /usr/bin wholesale — bash, grep, mkdir,
and tar included — and execFileSync('bash') died of ENOENT: all seven
capture-tools tests failed in the Test (ubuntu-latest Node 22.x) job
while passing on tmux-less dev machines.

Replace the directory-level drop with an entry-level shadow: each
tmux-bearing directory is mirrored (symlinks) into a scratch dir minus
the tmux entry, in place, preserving PATH order and the empty-entry
stripping the old filter did. Hosts without tmux take the map through
unchanged, and Windows (no tmux in its PATH, no symlink branch) keeps
its current behavior exactly.

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

* fix(ci): capture-tools test review fixes — faithful stubs, env shape pins, missing-branch scenarios

Round-3 review findings: the harness executed several of the step's
branches but asserted nothing about them, so probe-verified mutants
(dropped tmux guard, deleted warning/degradation messages, malformed or
missing FREEZE_VERSION/FREEZE_SHA256, wrong hash variable, dropped URL
`v` prefix or curl `-L`, severed tarball paths, broken regex boundary,
leaked mktemp dir) all shipped green.

- Make the curl/sha256sum/tar stubs model their real contracts: exact
  pinned URL, pinned checksum over a file curl actually wrote, existing
  -xzf operand
- Pin FREEZE_VERSION/FREEZE_SHA256 shape in captureToolsSource
- Pin the full curl flag set and the three-site tarball path agreement
- Assert the stale-renderer warning (fires on degraded re-download,
  silent on the happy path) and the tmux-unavailable message
- Pin TMPDIR and assert the mktemp cleanup leaves it empty
- Add the two missing scenarios: tmux-present skips apt, cached version
  extending the pin with a leading digit re-downloads

Verified by 13 mutation probes: every named mutant now turns the suite
red (13/13 killed), baseline 34/34 green.

* fix(ci): capture-tools step review fixes — hash-verified cache, per-run PATH promotion

* fix(ci): capture-tools step review fixes — verified-bytes-only installs, step timeout

Review findings on the capture-tools step:

- Drop the PATH-trust branch: a freeze already on PATH was accepted on
  its own --version and executed to probe it — exactly the self-report
  the FREEZE_BIN_SHA256 comment declares attacker-controllable, from
  dirs writable between jobs on both runner classes. The checksummed
  download always runs now; the cache makes it free after the first
  run.
- Guard $tools_bin in the download branch: with mktemp failing, the
  unguarded install resolved to /freeze — harmless unprivileged, but a
  root-in-container self-hosted runner writes it and reports success
  with nothing on PATH.
- Copy-then-verify the cache: install into the fresh per-run dir FIRST,
  verify THOSE bytes, delete both copies on mismatch — the verified
  bytes are the bytes later steps execute, closing the check-then-copy
  race for free. This makes the separate pre-verify block redundant;
  it is deleted.
- Add timeout-minutes: 5 — continue-on-error bounds failure, not
  duration, and a stalled `sudo apt-get update` mirror had no other
  bound under the 300-minute job cap.
- Report block: say the resolved freeze is likely broken when its
  --version produces nothing, instead of echoing a blank line and
  calling it stale; the mismatch wording is direction-neutral now.

Tests: replace the PATH-trust scenario with a planted-PATH one (marker
outside the scenario dir proves the plant never executes), add the
mktemp-failure scenario (the install stub succeeds like root would, so
the unguarded mutant is caught) and the promoted-dir 0700 assertion;
re-anchor the two digit-boundary tests on the report's warning. 41/41
green; both fix mutants verified killed.

* fix(ci): capture-tools review fixes — stale-dir cleanup, pinned guards

Address round-5 review:
- R5-1 (Critical): the per-run qwen-review-tools.* dir under RUNNER_TEMP
  was never removed; RUNNER_TEMP survives across jobs on the shared pool,
  so every review run accumulated one dir + one Go binary, unbounded.
  'Clean stale agent state' now removes stale dirs before the install
  step creates the current run's dir, matching the qwen-triage.yml
  convention. The harness comment claiming the dirs were runner-cleaned
  is corrected.
- R5-6: the cache re-verification rejection branch now logs why it
  deletes the cached binary instead of degrading silently.
- R5-7: bump-checklist note beside the freeze pins — the harness stubs
  key on the same env values, so a transposed hash pair must be caught
  against the real release artifacts at bump time.
- R5-2/R5-3/R5-4/R5-5: four unpinned step properties now pinned (the
  if: guard, the sudo -n probe flag, install-after-context ordering, and
  the cache branch's tools_bin guard via a new mktemp-fails scenario);
  six mutation probes confirm each pin kills its mutant.

* fix(ci): capture-tools review fixes — curl budget, swept scratch dir, wiring pins

* fix(ci): capture-tools review fixes — harness mutation pins, pin-pair self-check

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

* fix(ci): capture-tools review fixes — shadow-farm cleanup, backoff budget term

* fix(ci): capture-tools review fixes — report probes only installed freeze, age-gated sweep

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-08-05 13:16:56 +00:00
Shaojin Wen
1a2bb10298
fix(review): admit evidence images by content, not by name — magic-byte sniffing (#8459)
* fix(review): admit evidence images by content, not by name — magic-byte sniffing

The publish-assets allowlist was extension-based, and an extension is
a claim anyone can make: combined with a prompt-injected review run,
whatever could name a file evidence.png could host up to the size cap
of arbitrary bytes at a github.com URL through the evidence push
(raised in #8454's review as an enablement consideration).

sniffImageFormat reads the four admitted signatures (PNG, JPEG,
GIF87a/89a, RIFF+WEBP — RIFF alone is not enough, AVI and WAV share
the container prefix) and validateAssetContent rules the content
against the format the extension claims, fail-closed: an unrecognized
signature refuses even when the extension is allowed. publish-assets
applies the ruling to every file's first bytes before anything is
uploaded — all-or-nothing, same refusal contract as the other gates.

Pinned: the four signatures, truncated/empty headers, the RIFF/AVI
near-miss, extension-format mismatch, fail-closed unknown extensions,
and end-to-end: a shell script named evidence.png refuses with exit 3
and nothing pushed.

* fix(ci): re-pin review timeout tests to vars externalization (#8459)

* fix(review): single-source the asset allowlist and pin sniff depth (#8459)

Address round-1 review suggestions:

- ASSET_EXTENSIONS now derives from EXTENSION_FORMAT, so admitting a
  format is a one-place change the batch gate and the content gate
  cannot drift on; the duplicated extension extraction moves into one
  claimedExtension helper shared by both gates.
- Pin what the mutation probes showed unpinned: the GIF87a branch
  (sniff + admission), uppercase extensions at the content gate, the
  refusal message direction, the full depth of every signature
  (near-miss negatives), and the publish-time 16-byte slice end to end
  (a WEBP publishes through runPublishAssets; verified the test fails
  when the slice shrinks to 8).

* fix(review): pin every sniff check and guard the allowlist lookup (#8459)

* fix(review): pin every sniff byte and the two-gate format coupling (#8459)

* fix(review): align the two-gates comment with the pin that enforces it (#8459)

* fix(review): sharpen evidence-gate diagnostics and pins (#8459)

- Content refusals now name the JSON-quoted full path, so two same-named
  files from different directories are tellable apart (the sibling
  read-error refusal already spent the path).
- Export ASSET_HEADER_BYTES from the lib that owns the sniff depth; the
  publish call site uses it and the two-gates pin slices canonical headers
  to it, so a future longer signature fails the pin, not real publishes.
- One shared refusal builder keeps the allowlist message identical in
  validateAssetFile and validateAssetContent.
- Narrow the sniffImageFormat threat model to what magic bytes buy: binds
  the claimed type to the leading bytes, does not stop prefixed payloads.
- One-byte-off matrix becomes a labeled it.each table (failures name the
  exact corrupted byte); imports re-alphabetized.

* test(review): pin the shared extension refusal across both asset gates (#8459)

* test(review): pin lastIndexOf extension parsing for multi-dot asset names (#8459)

* fix(review): admit WEBP by its fourcc and name refused files once (#8459)

---------

Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-05 02:23:25 +00:00
jinye
32e2741577
perf(core): clear tool results to a low watermark to preserve prompt cache (#8464)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
* perf(core): clear tool results to a low watermark to preserve prompt cache

Size-triggered microcompaction now clears oldest compactable tool results down to half the threshold instead of stopping just below it, so the conversation prefix stays stable between clearings and provider prompt caches keep matching. The recent-result budget now protects committed results only; pending results no longer consume protection slots but stay counted, uncleared, and live for file-read-cache resolution. Adds the watermark to cleanup metadata and the debug log.

Fixes #8463

* fix(core): harden size-cleanup protection against zero-char and pending refs

Review follow-up for the low-watermark change: keepRecent now selects from committed results that are actually clearable (positive, successful, uncleared output), so trailing errors, prior placeholders, and empty outputs no longer absorb protection slots. Pending refs are dropped from the keep set entirely — a pending read may be a cache-hit placeholder rather than file bytes, so it must not suppress eviction reporting; over-disarming only costs a redundant re-read. Adds regression tests for both plus the protected-saturation consecutive-trigger corner.

* qwen: address PR review feedback (#8464)

Pin the (soft-exceeded) log marker with the one-line assertion suggested by the sandboxed verification report (finding S-1): the all-protected overage test now asserts 'target 250000 (soft-exceeded)', killing the surviving mutant M4.

* qwen: address PR review feedback (#8464)

* qwen: address PR review feedback (#8464)

* qwen: address PR review feedback (#8464)

Two P1 context-integrity fixes from review: (1) media-only tool results (image/PDF reads with empty text output and bytes on functionResponse.parts) stay in the idle-path keepRecent candidates instead of being dropped by the zero-char filter; (2) only write_file results vouch for file residency in kept-path accounting — edit calls carry just old/new snippets while still setting the cache's sticky full-read flags, so a kept edit can no longer suppress eviction reporting after the full read is blanked. Regression tests for both.

* qwen: address PR review feedback (#8464)

Pin the absence of the (soft-exceeded) marker at the exact watermark boundary: clearing that lands the virtual total exactly on the watermark must not be flagged. Kills the >= and always-true mutants of the marker condition that previously survived the suite.

* qwen: address PR review feedback (#8464)

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-04 19:00:11 +00:00
ChiGao
5631f4b112
feat(serve): add a required external tool guard provider (#8125)
* feat(serve): add required external tool guard

* fix(serve): keep guard constants off fast path closure

* test(serve): cover guard startup options

* refactor(acp-bridge): centralize external tool guard validation and ack value (#8125)

* fix(core): align MCP reconnect timeout test with safe replay policy (#8125)

The reconnect-on-timeout test still built its mock tools without server
trust or tool annotations, which the safe replay change now requires
before automatically replaying a connection-loss failure. Update the
fixtures the same way the surrounding reconnect tests were updated,
keeping the test's original assertion that a timeout on a known
disconnected server goes through the reconnect path. Mirrors the same
alignment already landed on main.

* fix(cli): alias externalToolGuard subpath for vitest source resolution (#8125)

This PR added `@qwen-code/acp-bridge/externalToolGuard` imports to cli
serve/acp modules but not the vitest source alias every other acp-bridge
subpath carries. Without it, any vitest run whose acp-bridge dist is
stale or absent fails to resolve the import and the five serve test
files die at transform time. Add the alias following the documented
convention in the config so tests read the live source.

* fix(serve): reject non-ASCII external tool guard bearer tokens

A token outside the ASCII range passed construction but made the
handshake throw ERR_INVALID_CHAR when interpolated into the
Authorization header, blocking qwen serve startup in required mode
with an unexplained error. Enforce printable ASCII (0x21-0x7E) at
validation time so the configuration fails fast with a clear message.

---------

Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-04 14:37:13 +00:00
Shaojin Wen
6e9ecc41e4
fix(core): resolve DashScope thinking-knob conflicts by family (#8488 round 2) (#8536)
Second review round on #8488:

- Honour an explicit extra_body enable_thinking: false on the qwen3.8-max
  family as reasoning_effort: 'none' instead of silently deleting it and
  re-enabling thinking
- Legacy qwen hybrids now drop the inert reasoning_effort override when it
  conflicts with a meaningful thinking_budget, keeping the knobs the model
  actually reads
- Family-gate the pipeline's enable_thinking tool_choice clause like the
  reasoning_effort clause: on non-qwen models sharing the endpoint the
  field is an opaque no-op (GLM reads thinking.enabled), and stripping
  forced tool selection there degraded their side queries
- The tier-native disable path emits reasoning_effort: 'none' — the knob
  the family reads — instead of enable_thinking: false; the
  required-thinking retry trigger recognises the new shape so runtime
  learning still fires
- Warn once per generator (not debug per request) when user extra_body
  knobs are dropped; hoist the wire-model family predicates to
  modalityDefaults.ts and share the provider's extra_body merge tail
- Tests for every behavior above (all load-bearing, verified by targeted
  mutation); docs attribute the vendor rejection to thinking_budget only
  and document the extra_body exceptions
2026-08-04 14:27:08 +00:00
Shaojin Wen
ac67de2e46
fix(review): stop the reverse-audit loop while there is still time to report (#8468)
* fix(review): stop the reverse-audit loop while there is still time to report

Measured on CI run #8368 (+1699 lines): the iterative reverse audit ran to
its 5-round cap, each round a per-chunk fan-out whose findings then went
back through verification, and the loop consumed 3.5 of the job's 4
budgeted hours. The outer GNU-timeout kill arrived while round 5's
findings were still being verified. The review died holding every
confirmed finding it had; nothing reached the pull request.

The loop's rounds are driven by the orchestrator, but every round begins
at the same place: agent-prompt building the round's prompts. So the
builder becomes the loop's clock. When the environment carries a review
deadline (QWEN_REVIEW_DEADLINE_EPOCH, exported per attempt by the review
workflow) and the remaining time is inside the reserve kept for the last
verification, compose-review and submission (default 60 minutes,
QWEN_REVIEW_DEADLINE_RESERVE_SECONDS to override), a reverse-audit round
is refused: a BUDGET line on stderr, exit code 4, no prompt built and no
record written. The message carries the exact unreviewedDimensions entry
to file, so the disclosure that caps the verdict is the CLI's text, and
Step 6 proceeds with the findings already confirmed.

Local runs have no deadline and are untouched. A malformed deadline fails
open — the outer kill still bounds the run, and a broken variable must
degrade to today's behaviour rather than wedge every budgeted review at
round 1. The verifier is deliberately not gated: the reserve exists so it
can run.

* fixup: scale the deadline reserve to the externally-chosen budget

The budget is not this workflow's to assume: it arrives from a repository
variable, a workflow input, or a /review --timeout=N comment. A fixed
60-minute reserve would consume most of a 70-minute budget and refuse the
audit loop outright on a 30-minute one. The workflow now passes a reserve
of a quarter of the attempt, floored at 10 minutes and capped at 60; the
CLI constant remains only the fallback for a caller that sets a deadline
without a reserve.

* review feedback: admit the round only if IT fits, and cap deterministically

Three findings from review, all taken:

1. The gate budgeted for the tail but not for the round it admits — the
   terminal round is by construction the one that starts closest to the
   boundary, so the killed-mid-verification failure survived one round
   wide. The gate now requires remaining >= round + reserve, where the
   round's cost is the previous round's, measured admission-to-admission
   from a stamp the builder writes (one per round; a same-round rebuild
   is not a round), falling back to a 30-minute constant for round 1,
   which starts with the most headroom.

2. The refusal was deterministic; the disclosure that caps the verdict
   was prose the orchestrator had to carry. The builder now records a
   budget-stop marker beside the prompt records and compose-review
   synthesizes the unreviewedDimensions entry from it — deduped against
   a relayed copy — so a run that drops the sentence still cannot
   approve past a truncated audit.

3. Exit code 4 is documented in the command's describe.

Also restores the Step 5 bullet the previous commit's edit displaced
(new findings merge into the cumulative list before the next round).

* review feedback: pin the budget gate's all-chunks refusal and ordering

Cover the two behaviours the review noted were only asserted on the bare
--findings form: an exhausted budget refuses the loop's real
--all-chunks round before ANY of the per-chunk records is written, and
a malformed call (--round 0) still gets its validation error first —
exit 4 is for a well-formed round the budget refuses, never a
replacement error. Also name what the code already does: reserve=0 is
the deliberate escape hatch (the gate shrinks to the round estimate
alone), and the workflow's 3600s cap mirrors DEFAULT_RESERVE_SECONDS.

* docs(review): describe the soft-deadline env vars for time-budgeted runs

The review noted the two new variables appeared in no user-facing doc;
the reserve in particular is an operator-facing knob. State what each
does, the fail-open posture, and how the refusal surfaces in the verdict.

* fix(cli): align budget-stop disclosure with the gate's refusal (#8468)

A round-1 budget refusal left no reverse-audit records, so the Step 4/5
floor reported the deliberate stop as a rogue/unlaunched audit with a
rebuild FIX the same gate deterministically rejects; the refusal's own
disclosure was swallowed by the caller-echo dedup. The floor now stands
down when the budget-stop marker exists, and compose-review renders the
disclosure structurally, bilingually, from the marker.

Also: `--role reverse-audit` requires `--round <k>` (an unlabeled
admission stamps an entry no estimate can attribute), the budget gate
runs after the plan/findings reads (a broken plan or unreadable findings
deserves its own error, and nothing is stamped ahead of a buildable
call), and the gate's admission boundary, measured-cost behaviour, and
the workflow env contract are pinned by tests.

* review: a budget stop excuses only the round it refused

The budget-stop suppression keyed on the marker's existence alone, so
every reverse-audit gap shape went silent once any round was refused —
including the shapes that describe rounds which RAN before the budget
hit. A hand-written round-1 launch is exactly as undelivered when round
3 later hits the budget, and suppressing its disclosure let 'stopped
before round 3' imply the rounds that did run were faithful.

Exactly one shape is by design under a marker: not-built — the refusal
writes no record, so an audit with no records IS the audit the gate
stopped, and its FIX (rebuild the round) would be refused by the same
gate. The suppression now names that shape and no other; a rewritten,
unlaunched or brief-unread round keeps its disclosure and its repair.

The new test pins the operative halves: the verdict stays capped, the
marker's disclosure posts, and the operator channel carries the
rewritten round's exact repair. (The posted body collapses same-subject
disclosures — both say 'reverse audit' — so the author sees the stop;
repairs are acted on from stderr, where the rewritten fix rides.)

* fix(review): fence budget state per run, and let gate errors beat budget stops

Address the round-2 review threads on the reverse-audit budget gate:

- Fence budget-rounds.json and budget-stop.json by the plan's own mtime.
  Every run rewrites the plan at its Step 1 capture, so records older than
  the plan belong to a previous run of the same PR: a run killed before
  cleanup no longer prices the next run's rounds off stale stamps (an
  hours-old stamp read as an hours-long round refused round 1 of a fresh
  budget) and no longer caps a later run's verdict on a stop that did not
  happen in it (R2-1, R2-2).
- Refuse a structurally unbuildable plan (no chunks[], duplicate or
  non-integer ids) with its own error ahead of the budget gate, so the
  same corruption gets the same diagnosis whatever the clock says, and no
  budget-stop marker is written over a corrupt plan (R2-5).
- Stamp a round admitted only after its build succeeds: a build that
  throws leaves no stamp, so the next round's cost is never measured from
  a build that produced nothing and floored to 600s (R2-6).
- Keep the budget entry's 'reverse audit' subject out of the caller-echo
  prefix filter: other reverse-audit scopes the orchestrator disclosed
  (a twice-whiffed chunk from the rounds that DID run) are no longer
  silently dropped in the marker's shadow; the marker's own relays stay
  deduped by the phrase splice (R2-7).
- Render --round unbracketed in the reverse-audit rebuild fix — the CLI
  refuses a round-less reverse-audit call, so the paste-and-run repair
  must not present the flag as optional (R2-14).
- Document the deliberate one-verification overlap between the measured
  round estimate and the tail reserve, at both definitions (R2-13).
- Test hardening, each assertion mutation-probed to fail its named
  mutant: a reshaped relay only the marker-phrase splice dedups (R2-8);
  the stamp's round label and the verifier's no-stamp invariant (R2-9);
  whole-line, unit-arithmetic and reserve-cap pins on the CI wiring
  contract (R2-10); the first-wins stamp survivor (R2-11); the reserve=0
  escape hatch (R2-12).

---------

Co-authored-by: verify <verify@local>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-04 13:58:15 +00:00
Shaojin Wen
874e46d734
fix(core): harden Qwen 3.8 reasoning effort wire shape (#8488)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
* fix(core): harden Qwen 3.8 reasoning effort wire shape (#8472 follow-up)

Follow-up to #8472, addressing the post-merge review findings:

- Drop enable_thinking/thinking_budget after the extra_body merge whenever
  reasoning_effort ships: the Token Plan preset made qwen3.8-max-preview
  carry both thinking knobs, and DashScope rejects reasoning_effort
  combined with thinking_budget
- Family-gate the new tool_choice=required strip clause to qwen wire
  models: reasoning_effort is an opaque sampling override on non-qwen
  DashScope models, and dropping forced tool selection degraded their
  structured side queries
- Prefix-match the qwen3.8-max family so dated snapshots and -latest
  aliases receive the selected tier instead of silently collapsing to
  enable_thinking
- Log the tool_choice strip; restore the effort-config JSDoc and comment
  the request-level override copy

* fix(core): family-gate DashScope thinking-knob drop (#8488)

---------

Co-authored-by: qwen-code-ci-bot <qwen-code-ci@service.alibaba.com>
2026-08-04 12:42:02 +00:00
Dragon
48d37cdf70
docs: document headless Goal workflows (#8503) 2026-08-04 03:30:26 +00:00
Dragon
1c1ee23f93
feat(core): support Qwen 3.8 reasoning effort (#8472)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
2026-08-03 19:45:11 +00:00
Shaojin Wen
7dfc554dff
feat(review): Add structured Web Shell review results (#8402)
* feat(review): add Web Shell review artifacts

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

* test(web-shell): add code review artifact visual scenario (#8402)

* fix(review): address Web Shell review artifact feedback (#8402)

* save-artifact: document why paths resolve against the daemon workspace
  root (QWEN_CODE_PROJECT_DIR) instead of cwd, and cover the relative-path
  form the skill documents with a test where the two roots differ.
* CLI/renderer contract: the renderer hand-duplicates the findings
  vocabulary and fails closed on unknown values, so name the renderer as a
  second consumer beside the CLI's lists and check in a contract fixture
  generated through the real pipeline (validateFindings -> buildReport ->
  save-artifact) that exercises every source, severity, confidence and
  outcome. Exporting the vocabulary through the SDK stays deferred: it is a
  public cross-package API change beyond this PR's seam.
* resolve-anchors now validates `line` exactly like `findings` does
  (positive safe integer); the two validators in one pipeline no longer
  disagree. Note: an in-flight `.qwen/tmp` findings file carrying `line: 0`
  fails where it previously did not.
* The renderer validates markdownReportPath (relative, no ".." segments,
  .md suffix) before it becomes a readWorkspaceFile call, resets the
  severity/confidence filters when switching artifacts, and surfaces
  heldByMeasurement so a nonzero Held count is attributable.
* save-artifact refuses low effort structurally (choices and library guard)
  instead of by prose, stats the Markdown report before reading it so a
  directory reports "not a file", and the component no longer shadows the
  DOM `document` global.
* The case-insensitive alias test now skips visibly on case-sensitive
  filesystems instead of passing vacuously.
* Comment the kept `turnOutputs.review` key and document the JSON
  companion in the user docs.

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

* fix(review): address second Web Shell review artifact feedback round (#8402)

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-03 16:08:13 +00:00
jinye
0cb109f513
fix(core): Avoid replaying unsafe MCP tool calls (#8387)
* fix(core): Avoid replaying unsafe MCP tool calls

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

* fix(core): Revalidate MCP replay after reconnect

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-03 11:04:38 +00:00
tomatotomata
2ae8cd9619
feat(memory): configure background agent turn limits (#8171)
* feat(memory): configure background agent turn limits

Signed-off-by: ahmadalguydi <ahmadalgaidy@hotmail.com>

* fix(memory): honor configured skill review turn limits

Signed-off-by: ahmadalguydi <ahmadalgaidy@hotmail.com>

* test(memory): cover planner turn limit sentinels

* fix(memory): apply turn limits to all agents

* test(memory): cover remaining turn limit feedback

---------

Signed-off-by: ahmadalguydi <ahmadalgaidy@hotmail.com>
2026-08-03 03:56:43 +00:00
Shaojin Wen
186812694c
feat(review): publish evidence images to a user-designated assets repo (#8351)
* feat(review): publish-assets — evidence images for PR review comments

GitHub's API cannot attach images to review comments (the web UI's
drag-and-drop upload has no API equivalent), so a review whose evidence is a
screenshot — a TUI rendering, a before/after comparison — had no way to show
it. New `qwen review publish-assets` hosts evidence images in a
user-designated repository and hands back URLs a comment can embed.

Grew from the maintainer's manual workflow (screenshots pushed to
`pr-assets/<PR>-verify` branches over HTTPS), and inherits the shape of the
skill's only other public write (`submit`) deliberately:

- Designated destination: writes only to QWEN_REVIEW_ASSETS_REPO, an
  owner/repo the user set by hand — the reviewed repo for maintainers, a fork
  or scratch repo otherwise (fork-vs-in-repo becomes a configuration
  difference, not two code paths). A separate variable from
  QWEN_REVIEW_SCRATCH_REPO on purpose: that contract forbids PR-derived
  content, and evidence screenshots are exactly that. Unset → exit 3.
- Authorised run: the same args-file re-parse and target binding as submit,
  now extracted to a shared lib/authorization.ts so the two gates cannot
  drift (the target-binding lesson lives in one place). Since an effective
  --comment forces high effort, low/medium runs can never publish.
- Images only, capped, all-or-nothing: extension allowlist (SVG excluded — a
  script container), per-file and per-batch size caps, one refused file
  refuses the batch before anything is pushed.
- Immutable references: files land on pr-assets/<pr>-review via the Contents
  API (HTTPS via gh; no clone, no SSH), content-hash-named so re-runs are
  idempotent, and every URL is pinned to the commit — a posted comment's
  evidence cannot be changed from under it. The web-host /raw/ URL form works
  unchanged on GitHub Enterprise.
- Auditable: a manifest names every file pushed and the landing commit,
  swept by cleanup with the other review artifacts.

The findings artifact gains per-finding `assetFiles` (local evidence paths)
and `assets` (published URLs); `publish-assets --findings/--findings-out`
publishes everything referenced and weaves the URLs back in, so the comment
builder reads the artifact rather than hand-carrying URLs.

What the command cannot check is stated in SKILL.md instead: image content.
Publish only evidence the review itself produced — never a capture of the
user's own terminal, which can hold an env dump in the scrollback.

Tests: 45 files / 1394 assertions — new suites for the assets naming and
validation rules and the command's gates (refusal without designation,
refusal without authorisation, target binding, branch creation, idempotent
re-run, batch refusal, findings weaving); submit's 42 pass unchanged on the
extracted gate.

* fix(review): publish-assets round-1 self-review — six findings

Round-1 review of this branch, walked with the angles the author-side pass
does not cover:

- submit.ts kept its parseReviewArgs import after the authorization
  extraction; vitest does not typecheck, `tsc --build` does, and CI's build
  leg failed on TS6133. (The whole first CI round's failures cascade from
  this one break.)
- ensureBranch %2F-encoded the slashed ref path; GitHub's documented form is
  literal slashes and %2F routes inconsistently across endpoints — a 404
  here reads as "branch missing" and turns every re-run into a 422 on the
  create. Ref paths are now literal (the branch name is built from a
  validated integer, so interpolation is safe); the contents `?ref=` query
  VALUE keeps its encoding, which is the correct position for it.
- The authorization gate bound URL-shaped `--comment` arguments against the
  ASSETS repo, refusing legitimately authorised runs whenever the assets repo
  is a fork rather than the reviewed repo. The shared gate's repo binding is
  now optional — submit still always binds it; publish-assets binds the PR
  number (and host) alone, with a new optional --reviewed-repo to restore
  the stronger binding when the orchestrator knows the reviewed repo.
- URLs were pinned to the last PUT response's commit.sha; on an
  identical-content update that field's shape is GitHub's to decide, not
  ours to assume. The head is now read from the branch ref after the
  uploads — one extra call for independence from the response shape.
- putContent's catch-all retried EVERY failure through the exists path,
  answering a 401 with a confusing secondary error from the sha lookup; the
  retry now fires only on the 422/needs-sha shape and rethrows the rest.
- --findings without --findings-out silently skipped the URL weaving; it
  now warns, and --findings-out implies --findings.

New tests: literal-ref assertion, non-exists rethrow, URL-shaped
authorisation without assets-repo binding, --reviewed-repo mismatch refusal.
45 files / 1399 assertions green; `tsc --build` clean.

* fix(review): publish-assets round-2 — empty-findings no-op, reviewed-repo hint

Round-2 findings on this branch:

- A findings artifact carrying no assetFiles is the ORDINARY case for most
  reviews, but publish-assets answered it with exit 3 — a refusal an
  orchestrator calling the command unconditionally on every posting run
  would read as a failure to repair. It is now a no-op (exit 0,
  {published:false, count:0}); a bare --files with nothing named keeps the
  exit-3 refusal, because there the emptiness IS the caller error.
- SKILL.md's example now names --reviewed-repo for URL-target reviews, so
  the stronger authorisation binding is used where the orchestrator knows
  the reviewed repo.

44 files / 1387 assertions green; tsc --build clean.

* test(review): fix invalid two-argument expect in assets.test.ts

Round-3 sweep: vitest's expect takes one argument — the message-style second
argument was a lint error and a weak assertion both. The offending value now
rides inside the asserted object, so a regression names which shape slipped
through instead of reporting 'expected true'.

* test(review): pin the findings schema's evidence-asset validation directly

Round-4 sweep: assetFiles/assets were exercised only through publish-assets'
weaving test — the schema's own rejection paths (non-array, empty-string
entry, empty-array drop) had no direct case, so a validation regression
would have surfaced as a confusing weaving failure two layers up.

* fix(review): address all six findings from the automatic review (R1-1..R1-6)

The /review pipeline's own round-1 findings on this PR, each confirmed and
fixed:

- R1-1 (the real catch): the host-binding check sat nested inside the
  `req.repo !== undefined` guard, so a caller omitting --reviewed-repo also
  silently skipped the HOST binding — contradicting the documented "binds
  the PR number (and host) alone". The host check now stands on its own;
  a new test pins an Enterprise-host mismatch refusal with the repo binding
  absent.
- R1-2: --pr accepted whatever yargs `type:'number'` passed through (NaN,
  0, 3.5), and --user-authorized bypasses the gate that would have
  re-parsed the target — `pr-assets/NaN-review` was reachable. A Gate-0
  positive-integer check now refuses first, matching submit's sibling
  discipline.
- R1-3: the suite drove the skillArgs seam without clearing
  QWEN_CODE_SESSION_ID, so running it inside an active Qwen Code session
  spuriously failed eight tests. beforeEach now saves/clears the variable
  and afterEach restores it.
- R1-4: the 40MB aggregate cap was enforced inline and untested (a mutation
  deleting it stayed green). The per-file rules and the total cap now live
  in one pure ruling, validateAssetBatch, unit-tested with five 9MB sizes
  and no fixtures.
- R1-5: the asset_files snake_case alias was the one untested member of an
  otherwise-tested alias family; pinned.
- R1-6: the setGhHost wiring had no command-level assertion; a GHE test now
  pins both the call and the host-carrying manifest URLs.

44 files / 1397 assertions green; tsc --build and eslint clean.

* fix(review): address all ten round-2 findings from the automatic review

Round-2 of the /review pipeline on this PR: 2 Critical, 8 Suggestions,
every one confirmed against the code and fixed.

Criticals:
- The round-2 test block added for the empty-findings no-op omitted the
  QWEN_CODE_SESSION_ID save/delete/restore its two sibling blocks perform,
  so the suite spuriously failed inside an active Qwen Code session — the
  exact dogfooding environment this repo reviews from.
- The gh routing and the returned URLs read the host from two different
  sources: with --host absent, gh children inherit an operator-exported
  GH_HOST (routing at Enterprise) while rawAssetUrl defaulted to
  github.com — every returned URL a 404. One effectiveHost (flag, then
  GH_HOST env) now feeds both.

Suggestions:
- putContent's retry discriminator matched a bare `422` anywhere in
  err.message — which execFileSync fills with the full command line,
  including the PR-numbered remote path: evidence for PR #4220 would read
  a 401 as "already exists". Anchored to `HTTP 422`.
- ensureBranch's bare catch read every ref-lookup failure (401, 403
  rate-limit) as "branch missing"; only HTTP 404 takes the create path
  now, and an empty assets repo — whose default_branch resolves while its
  head ref 404s — is named as the condition it is, with the fix stated.
- Validation refusals threw (yargs exit 1, stack trace, empty stdout)
  while every other gate in the command answers exit 3 +
  {"published": false}; unreadable files and batch refusals now speak the
  same refusal language.
- The command's idempotent writes (content-hashed PUTs, a ref create
  whose duplicate is tolerated) now go through a new ghWithInputRetried —
  sharing gh()'s transient-error retry — and ghWithInput's no-retry
  docstring names the two-caller split instead of claiming a sole caller.
- parseAssetsRepo admitted dot-segment repos (`owner/..`) its docstring
  claimed were path-safe; segments now exclude `.`/`..`, mirroring
  submit's isRepo.
- stringArray accepted whitespace-only evidence paths; trim(), matching
  the sibling asString.
- The GHE test asserted setGhHost was called but not WHEN; it now asserts
  the call precedes the first API invocation.

44 files / 1403 assertions green; tsc --build and eslint clean.

* refactor(review): one refusal helper for every publish-assets gate

Round-2 of this branch's fresh review: the refuse() helper existed below
seven inline copies of the identical three-line refusal — the drift shape
where one site eventually forgets the exit code. Hoisted to the top of
runPublishAssets and used by every gate; message content unchanged where
tests pin it. 26/26; tsc clean.

* fix(review): address the round-3 review — bidirectional host binding and 14 more

The automatic review's third round on this PR: 1 Critical + 14
Suggestions, each verified and addressed.

The Critical (host binding, both halves):
- The gate's `req.host &&` guard bound the host in one direction only —
  an Enterprise-URL authorisation admitted a host-less write routed at
  github.com (or wherever GH_HOST pointed). The gate now compares the
  authorised host against the write's EFFECTIVE host, defaulting an
  absent req.host to github.com: a host is a host, not an exemption.
- Both callers fed the gate the flag rather than the route: publish-assets
  computed effectiveHost (--host ?? GH_HOST) AFTER the gate and bound
  args.host; submit bound args.host while its gh child inherited GH_HOST.
  publish-assets now resolves effectiveHost before Gate 2 and binds it;
  submit binds the same resolution.

The rest:
- pr-assets/<N>-review registered in the asset-branch cleanup workflow,
  per its own every-producer-must-be-added-here rule — a branch nothing
  deletes is permanent.
- ghWithInputRetried had been inserted between ghWithInput and its JSDoc,
  leaving the does-NOT-retry comment attached to the function that DOES
  retry; each function now carries its own doc.
- putContent's retry-path contents-GET is wrapped: when the 422 was not
  the sha-missing shape and the path does not exist, the GET's 404 no
  longer replaces the PUT error the user needs.
- stringArray treats null as absent like every sibling parser, so an
  artifact rendering "no assets" as null canonicalizes instead of
  crashing.
- Test isolation, all four describe blocks: GH_HOST save/delete/restore,
  setGhHostMock.mockReset (a sibling's persistent throwing implementation
  survives mockClear — the malformed-host test also switched to
  mockImplementationOnce), and full mock resets in the blocks that lacked
  them.
- The two regression-pin tests the review measured vacuous now
  discriminate: each fails only the one call under test and asserts the
  pipeline stopped THERE (no contents PUT after a bad create; exactly one
  gh call after a 403 lookup).
- New positive pins: a double-fired create ("Reference already exists")
  succeeds; the canonical report shape this command's own --findings-out
  writes round-trips; an Enterprise-URL authorisation refuses a host-less
  write while a github.com-URL one passes it.

Not changed: the finding that reverting the Finding-interface hunk leaves
tests green — the fields are type-level and their removal fails
tsc --build (the CI leg that caught this branch's own TS6133); a runtime
pin would duplicate what the round-trip tests already exercise.

47 files / 1495 assertions green; tsc --build 0 errors; actionlint clean
on the cleanup workflow.

* fix(review): address the round-4 review — empty-GH_HOST passthrough and four test pins

Round 4 came back COMMENTED (down from CHANGES_REQUESTED), 5 Suggestions,
0 Criticals — all five confirmed and fixed:

- An exported-but-empty GH_HOST ("" from an unset workflow var) survives
  `??`, being non-nullish: effectiveHost became "" and the gate compared
  the authorised host against "", refusing a legitimate github.com write.
  Both call sites now collapse an empty trim to undefined (`|| undefined`,
  parenthesized).
- The gate's URL-shaped repo/host binding was exercised only via
  publish-assets' suite; submit.test.ts now pins both directions of the
  host binding and the repo binding at its own call site.
- ghWithInputRetried had no retry-contract test; gh.test.ts adds the
  symmetric block to ghWithInput's does-NOT-retry pin (transient 500
  retried once then succeeds; non-transient 401 single call).
- The publish-assets mock aliased ghWithInput and ghWithInputRetried to
  one mock, hiding which variant a write used; they are two mocks now,
  and the happy path asserts the non-retrying variant is never touched.
- The Prepared interface's dead `name` field is gone.

46 files / 1476 assertions green; tsc --build and eslint clean.
2026-08-02 15:49:32 +00:00
Shaojin Wen
09d818867e
fix(review): follow output language in Tip lines and saved reports (#8370)
* fix(review): follow output language in Tip lines and saved reports

The /review skill's critical rule 2 already states that terminal output
follows the user's output language preference, but three areas lacked
explicit guidance, causing the model to output them in English even
with a Chinese output language configured:

- Follow-up Tip lines (e.g. "Tip: type post comments to ...") were
  specified as English templates with no translation instruction
- The Step 8 saved report file used English section headings and prose
- Step 6 labels (e.g. "Balanced review (effort: medium)") had no
  translation note

Add explicit output-language guidance at each point of use, with a
Chinese example for Tip lines and Chinese section heading examples for
the saved report. Command keywords (post comments, fix these issues)
stay verbatim since they are trigger phrases the user types back.

* fix(review): disambiguate findings-artifact language boundary and complete low-effort translation notes (#8370)

---------

Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
2026-08-02 11:48:58 +00:00
易良
eea0a2b3b2
fix(github-channel): recover interrupted inbound tasks (#8306)
* fix(github-channel): recover interrupted inbound tasks

* fix(github-channel): make inbound recovery bounded

* test(github-channel): cover delivery-failure lifecycle and audit-hit recovery

Add a direct test for the onTaskLifecycle failed/delivery -> reply_pending
transition and a recovery test for the publication-audit match-and-remove
path. Restore the blank line between the constructor and createInitialCursor.

* fix(github-channel): preserve cancelled inbound tasks and fail closed on bookkeeping (#8306)

* fix(github-channel): harden inbound task lifecycle against partial persistence failures (#8306)

* fix(github-channel): close crash-window duplicates and make tests load-bearing (#8306)

* test(github-channel): add recovery test for suppressed audit outcome (#8306)

* fix(github-channel): persist errorCommentPosted after post, add review test coverage (#8306)

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-02 07:27:44 +00:00
Dragon
e967cc9037
docs: document compaction and image model selection (#8348)
* docs: document auxiliary model selection

* docs: align model option labels

* docs: clarify image model HTTPS requirement
2026-08-02 06:44:54 +00:00
jinye
4338120100
feat(serve): resolve and report the daemon memory budget (#8245)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
* feat(serve): resolve and report the daemon memory budget

The daemon has no notion of how much memory it has. It samples its own
RSS and heap every five seconds, and polls the primary ACP child's RSS,
but there is no limit anywhere to divide those by: no cgroup read, no
heap-size limit, no ratio, no `limits.*` memory field. Every number it
reports is an absolute byte count with nothing to compare against, so
"how close to exhaustion is this daemon" cannot be answered from
`/daemon/status` at all.

Resolve one set of figures at boot and report them. Configured and
effective budgets are separate: the effective value is capped at resolved
cgroup or host memory, so an operator passing a budget larger than the
machine gets a denominator the machine can actually back, with the
discrepancy visible rather than silently resolved. A derived budget below
the documented minimum is reported as `insufficientMemory` rather than
clamped upward, which would have invented capacity that does not exist —
a 768 MB host would otherwise report a 1 GB budget and poison every ratio
computed from it.

`limits.memory` carries the static figures, including `legacyCeilingMb`:
the ceiling an ACP child receives today with no budget involved, so the
gap between current behavior and any future policy is measurable before
that policy exists. `runtime.memory` carries live counts and the advisory
per-child share at both the registered and the live child count.

Nothing here sizes a child. Dividing the pool by a workspace count is not
a sound policy on its own, and the advisory shares exist to show why: on
a 32 GB host with 25 registered workspaces and only the preheated primary
live, a registered-count divisor would cut that child from 16384 MB to
614 MB for memory no dormant workspace is holding, while the per-child
floor still lets 25 children authorise more than the pool. Registration
is not allocation; a real policy needs admission at spawn time keyed on
concurrently live children, and it needs this data to be designed
against.

Applying such a share is also a compatibility change even with no
refusals, since it alters child GC and OOM behavior — so it is not
something this change should slip in under the heading of reporting.

Refs #8182
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(serve): report honest memory counts and guard the registered share (#8245)

* fix(serve): reuse workspace snapshots when counting active children

Counting active ACP children from `listManaged()` is right — `list()` only
returns entries in `active` state, so a workspace mid-drain, mid-replacement,
or blocked still holds a live child that `list()` drops. But taking the count
by calling `getDaemonStatusSnapshot()` again per managed runtime undoes the
existing reuse of the primary bridge's snapshot, and `getDaemonStatusSnapshot`
rebuilds the whole session array on every call.

The second pass also reads the tree at a different instant than the rest of
the response, so `activeAcpChildren` could disagree with the session and
channel figures beside it.

Reuse the snapshots already taken instead, keyed by bridge, and fall back to a
fresh call only for a managed runtime the first pass missed — which is exactly
the non-`active` entry the `listManaged()` change exists to catch.

The reuse this restores was already guarded by a test asserting one snapshot
call per bridge, but that test resolves no memory budget, and the second pass
ran only on the budget path — so it stayed green while every production
`/daemon/status` call did the work twice. Added a case that resolves a budget
and asserts the same property; it fails against the previous commit with
"expected 1 times, but got 2 times".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(serve): address review feedback on daemon memory budget (#8245)

* fix(serve): import isValidMemoryBudgetMb in serve command (#8245)

* fix(serve): address review feedback on daemon memory budget (#8245)

* fix(serve): stub isChannelLive on serve test fake bridges (#8245)

* fix(serve): address review feedback on daemon memory budget (#8245)

- Make the stderr-gate test host-independent by pinning os.totalmem
  through a vi.mock toggle instead of reading the runner's cgroup
- Add a spawn-path constant parity test enforcing that
  getAcpMemoryArgs and legacyChildCeilingMb agree on the fraction
  and cap, converting the comment-only invariant into a test
- Narrow the mirror comment to name only the two constants that
  actually have spawn-path counterparts
- Accept availableMemorySource through the resolveDaemonMemoryBudget
  seam so the constrained path is testable end-to-end
- Report maxChildHeapMb alongside minChildHeapMb on the wire so
  clients can distinguish the 16 GB cap from a large host
- Move the listManaged/list comment to the computation it describes
- Add a cross-reference at the opts literal for the late-assigned
  daemonMemoryBudget field

* fix(serve): address review feedback on daemon memory budget (#8245)

* fix(serve): address review — split fraction constant, sharpen parity test, deduplicate error, populate bootstrap memory (#8245)

* fix(serve): address review — document maxChildHeapMb, make parity test order-independent (#8245)

* fix(serve): address review — split parity test into two files to avoid cold re-import timeout (#8245)

* fix(serve): address review — correct session count, compatibility scope, bootstrap memory docs (#8245)

* fix(serve): address review — align help text framing, document default cap, fix stale comment (#8245)

* fix(serve): address review — add positive memory-budget validation test, correct activeAcpChildren docs (#8245)

* fix(serve): address review — document childRssBytes stale tail after watcher detach (#8245)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Qwen Autofix <qwen-autofix@users.noreply.github.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-02 04:19:31 +00:00
qwen-code-dev-bot
8d6d2ab56a
feat(cli): /summary supports custom export path (#8116)
* feat(cli): /summary supports custom export path (#8113)

`/summary` now accepts an optional path argument, matching `/export`'s
behavior. When a path is provided, the summary is saved there instead
of the default `.qwen/PROJECT_SUMMARY.md`.

- `/summary` → saves to `.qwen/PROJECT_SUMMARY.md` (unchanged)
- `/summary docs/summary.md` → saves to `docs/summary.md`
- `/summary /absolute/path/summary.md` → saves to absolute path
- `/summary docs/` → saves to `docs/PROJECT_SUMMARY.md`

If the path is a directory (existing or ending with `/`), the default
filename `PROJECT_SUMMARY.md` is appended. Parent directories are
created automatically.

* fix(cli): summary custom path dir detection and i18n key (#8116)

* test(cli): assert relative display path in summary tests (#8116)

* fix(cli): summary path containment, early validation, and mkdir hardening (#8116)

* fix(cli): defer summary mkdir to save time so failed generation leaves no empty dir (#8116)

* fix(cli): normalize path separators in summary test and assert file content (#8116)

* test(cli): assert directory permission mode in summary test (#8116)

* fix(cli): resolve symlinks in summary path containment check (#8116)

* fix(cli): reject broken symlinks escaping project root in /summary (#8116)

* fix(cli): guard /summary overwrite and expand tilde in path (#8116)

* fix(cli): re-validate appended default filename for symlink escape in /summary (#8116)

* fix(cli): harden /summary symlink chain walk, file mode, and overwrite guard (#8116)

* fix(cli): address review feedback on /summary custom path (#8116)

- Fix CRLF false-negative in overwrite guard by normalizing line endings
- Allow overwriting empty pre-created files (zero-length bypass)
- Detect trailing separator on existing file and report clearly
- Log chmod failures via debugLogger matching exportCommand convention
- Add comment explaining mkdir mode asymmetry
- Update docs: /summary usage table and custom-path welcome-back note
- Add i18n key for trailing-separator error in all 9 locales
- Add tests for CRLF, empty file, and trailing separator cases

* fix(cli): address review feedback on /summary custom path (#8116)

- Skip symlink-escape check for the default .qwen/ target so a
  symlinked .qwen/ directory (shared team config, overlay mounts)
  keeps working, and the check no longer runs after the LLM call
- Re-run the overwrite guard immediately before writing to close
  the TOCTOU window across the slow generation step
- Determine isDefaultTarget by comparing the resolved path against
  the default so `/summary .qwen/` gets the same 0o700 permissions
- Only chmod 0o600 on file creation; preserve existing permissions
  on regeneration
- Return empty content in interactive-mode errors to avoid double
  rendering (failInteractive already adds the error to history)
- Tighten the overwrite-guard regex to require `**Update time**: `
  after the Summary Metadata heading, preventing false positives
- Fix the realpathNearestExisting comment to document the missing
  containment-during-walk guard vs export/stats copies
- Add tests: symlink cycle, default target with symlinked .qwen,
  TOCTOU overwrite guard, explicit .qwen/ permissions, chmod
  preservation, interactive error content, regex false-positive

* fix(cli): address review feedback on /summary custom path (#8116)

* test(cli): cover post-LLM symlink re-check and interactive error UI (#8116)

* fix(cli): address review feedback on /summary custom path (#8116)

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-02 02:19:31 +00:00
Dragon
74dc5547f1
docs: complete TUI keyboard shortcut reference (#8327) 2026-08-01 17:24:19 +00:00
Shaojin Wen
dbb0349351
feat(review): borrow recall, a fix loop, and a size-derived budget from Claude /review (#8315)
* feat(review): borrow recall, a fix loop, and size-derived budget from Claude /review

Three changes, from a comparison of this skill against Claude Code's
`/code-review`. The orchestration half of that comparison went the other way —
nothing there has the worktree isolation, the transcript-backed coverage gate, or
the single computed verdict — so what is borrowed is what it does better:
how much it surfaces, what a finding *is*, and what a small diff costs.

Recall
------
The Exclusion Criteria are a filter on what KIND of thing is a finding. Read as a
confidence bar — which is how an agent under "silence is better than noise" reads
them — they license dropping anything half-believed, and that drop is invisible:
no later stage sees a candidate that was never filed. Every stage after the
finders removes wrong findings; none can add a missing one. Each finder brief now
carries the counterweight explicitly, and the Step 4 verifier deliberately does
not get it.

Code quality was one agent holding six unrelated checks — the shape this skill
already refuses for invariant agents, on measured evidence (PR #6457: one agent
with an eight-item checklist found 1 of 5 defects; the same model split three ways
found all 5). Split into 3a reuse/duplication, 3b altitude/abstraction fit, 3c
consistency/clarity. 12 -> 14 agents in 3A.

Low was one undirected pass capped at 8, and its only alternative was a
nine-subagent fan-out. It is now an angle rotation in one context — line-by-line,
removed behaviour, language pitfalls, wrapper routing, reuse/dead code, sibling
consistency, then a gap sweep — dedup-only, no re-judging, cap 10. Still zero
subagents.

--fix and findings as data
--------------------------
`--fix` is `--comment` reflected and gated on the opposite target: `--comment`
writes to a pull request, `--fix` writes to a working tree, so a PR review (whose
tree is the ephemeral worktree Step 9 deletes) ignores it with a warning. An
effective `--fix` floors the effort at medium — editing the user's files on an
unverified finding is the same mistake as posting one.

New `qwen review findings` canonicalizes the findings into a JSON artifact the
terminal report, the saved report and the review JSON all read, instead of three
transcriptions of one list. With `--outcomes` it merges the fixer's ledger and
REFUSES one that does not account for every finding: a fixer that applies six of
nine and reports six has not lied about any of them, it has silently shortened
the list.

Size-derived budget
-------------------
New `plan.budget`, computed from srcDiffLines the way the topology gate is and
recorded in the plan rather than passed as a flag, so every reader sees one
number. Scopes the low tier's angle count and sweep, the Agent 8 ceiling (0 below
80 source lines — "one domain dominates" is a judgement, and one made about forty
lines finds a dominant domain every time), and the verify shard width. It never
scales a dimension away: that is the roster's answer and the roster reads effort.

Not included: per-model prompt routing. Claude's table exists because it was
measured per model family; shipping an invented mapping into this skill is the
kind of change its own review would flag.

Tests: 39 files, 1215 assertions.

* docs(review): align counts and level descriptions left stale by the 3a/3b/3c split

Round-1 self-review findings on this branch:

- SKILL.md medium tier still named 'quality (Agent 3)'; the Step-1 low bullet
  and Step 3C heading hardcoded six angles though plan.budget scales them 3-6;
  and no fallback was stated for a plan written by an older CLI that carries no
  budget field (falls back to the flat pre-budget behaviour — more coverage,
  never less).
- DESIGN.md still labelled the 12-agent roster '(current)', kept the ten-lens /
  crosses-twelve topology arithmetic beside the updated fourteen-agent copy,
  keyed the re-gating cost row to the 12-agent roster, and described low as one
  pass ≤8 and medium as unverified inline angles — both contradicting the
  SKILL.md this PR ships. The LLM-call-budget and Fork-Subagent sections were
  still summed for 12 agents.
- findings.ts: validateFindings accepted outcome but dropped outcomeNote, so
  the canonical artifact did not round-trip — a skipped finding fed back
  through --input kept its outcome and lost its reason.

Tests: 40 review files green, including two new round-trip cases.

* docs(review): one id per finding across the cache ledger and the findings artifact

The rebase onto #8218 left two id schemes for one finding: the incremental
cache's cross-round ledger names findings R<round>-<n>, while the findings
artifact accepted any unique id. Same defect, two names, and the outcome
ledger and next round's report could no longer be joined. The artifact now
uses the R-ids whenever the run writes the cache ledger.

Conflict resolutions from the rebase itself: review.ts keeps both new
subcommands (test-delta from main, findings from this branch); the
documentation-parity check #8218 added to the old Agent 3 brief lands in 3c,
the consistency slice that owns sibling-parity checks.

* test(cli): add findings to the pinned review subcommand surface

review.test.ts pins the exact subcommand list and sits one directory above
the review/ glob the branch's local runs used, so the new findings
subcommand never met it until CI. Ubuntu was the only matrix leg that ran.

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-01 15:26:58 +00:00
Dragon
8673151ebd
docs: document skill learning and live reload (#8298) 2026-08-01 15:23:08 +00:00
qwen-code-dev-bot
ac4799feca
fix(cli): add ui.mouseTracking setting to restore right-click and URL clicks (#8198)
* fix(cli): add ui.mouseTracking setting to restore right-click and URL clicks

VP mode (default since 0.21.1) enables SGR mouse tracking (?1002h)
which captures ALL mouse events including right-click and left-click
on OSC 8 hyperlinks. The terminal can no longer handle these
natively — right-click context menus and clickable URL links stop
working.

Add a `ui.mouseTracking` boolean setting (default: true) that gates
SGR mouse tracking in useMouseEvents. When set to false, mouse
tracking is disabled entirely, restoring terminal-native right-click
and OSC 8 hyperlink handling. The trade-off is that mouse wheel
scrolling and scrollbar dragging in the VP viewport stop working;
keyboard scrolling (Up/Down/PgUp/PgDn) still works.

Users who need right-click or URL clicking can set:
  "ui": { "mouseTracking": false }

Fixes the regression reported in #8003 (right-click broken) and
the follow-up report (URL link clicks broken) after 0.21.1.

* fix(cli): regenerate mouseTracking schema and clarify setting scope (#8198)

* fix(cli): scope ui.mouseTracking to the TUI and document it (#8198)

* fix(cli): handle hyperlink clicks in VP mode via onContentClick

When SGR mouse tracking is active (VP mode default), the terminal
cannot handle OSC 8 hyperlink clicks natively. Add an onContentClick
callback to ScrollableList that fires on left-press in non-scrollbar
areas. MainContent implements this callback by extracting hyperlink
regions from the clicked item's text (parsing markdown links and
bare URLs with visual column tracking) and opening the URL at the
click position via the platform's open/xdg-open/start command.

This allows URL clicks to work in VP mode without disabling mouse
tracking, so mouse wheel scrolling and scrollbar dragging continue
to work simultaneously.

New file: packages/cli/src/ui/utils/hyperlink-click.ts
- extractHyperlinkRegions(): parses markdown links and bare URLs
  from raw text, computing visual column positions
- findUrlAtColumn(): maps a click column to a URL
- openUrl(): opens a URL via the platform's default handler

* fix(cli): remove broken hyperlink-click feature from VP mode (#8198)

Remove the hyperlink-click feature (hyperlink-click.ts, handleContentClick,
onContentClick) which had three critical defects identified in review:

1. Command injection on Windows: openUrl used child_process.exec with shell
   string interpolation, where single-quote escaping is ineffective against
   cmd.exe metacharacters (&, |, ^, ;).

2. Non-functional coordinate mapping: handleContentClick used raw 1-based
   SGR terminal screen coordinates directly as item indices into
   allVirtualItems, without the scroll-offset and frame-anchor transforms
   that every other mouse consumer applies.

3. Wrong visual-column accounting: extractHyperlinkRegions counted raw
   markdown characters (including **bold**, *italic*, ~~strikethrough~~
   markers) that InlineMarkdownRenderer strips, causing column drift.

The ui.mouseTracking setting (the PR's core purpose) is unaffected and
remains fully functional. A properly designed click-to-open feature can
be added in a follow-up with correct coordinate transforms, renderer-aware
column parsing, and openBrowserSecurely for safe URL launching.

* docs(cli): cross-reference ui.mouseTracking in useTerminalBuffer descriptions (#8198)

* fix(cli): document mouseTracking tradeoffs and hide showScrollbar in serve mode (#8198)

* docs(cli): clarify mouseTracking tradeoffs in shortcuts and settings (#8198)

Address review feedback: note that mouse-wheel scrolling requires ui.mouseTracking in the keyboard-shortcuts table, the tmux trackpad section, and troubleshooting; add the missing ui.showScrollbar row to the settings reference; and pin the mouseTracking schema fields (requiresRestart: false) in settingsSchema.test.ts.

* fix(cli): make mouseTracking restart-scoped and gate mouse affordances (#8198)

ui.mouseTracking declared requiresRestart: false but nothing propagates a
settings write to the refcounted SGR mouse mode: the SettingsContext value
is a single LoadedSettings created outside React with no change emitter, so
a subscriber that never re-renders (e.g. a memoized completed thought block)
keeps ?1002h active and right-click stays broken even with the setting off.
Mark it requiresRestart: true, matching ui.useTerminalBuffer, and drop the
runtime-toggle test whose own comment admitted it did not prove the flip
lands.

Add a shared useMouseTrackingEnabled hook and route the useMouseEvents gate
and the mouse-dependent affordances through it — the history-item "click to
expand" hint, the selection-list hover controller, and prompt
click-to-position — so they read one source and no longer advertise a dead
interaction when mouseTracking is disabled.

* test(cli): add component-level mouseTracking affordance gate tests (#8198)

* fix(cli): address review feedback on mouseTracking PR (#8198)

- Rewrite BaseSelectionList mouse test to mock RowMouseController and
  assert mount/unmount directly, pinning the component-level gate
  instead of the downstream useMouseEvents escape writes
- Add troubleshooting entry for right-click/links/selection symptoms
- Add useTerminalBuffer pairing advice to settings.md mouseTracking row

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
2026-08-01 14:15:42 +00:00
Shaojin Wen
6f8ad2b4a5
feat(review): Include CLI version in attribution (#8294)
* feat(review): Include CLI version in attribution

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

* test(review): Decouple footer test from package version

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-01 09:40:57 +00:00
Dragon
e569734a1e
feat(skills): add auto-skill curator (#7846)
* feat(skills): add auto-skill curator

* fix(i18n): translate /curator command descriptions for zh and zh-TW

The strict-parity locale coverage test failed because the new /curator
command and its status/run/restore subcommands fell back to English
descriptions in zh-CN and zh-TW. Add Simplified and Traditional Chinese
translations for the curator command descriptions and its user-facing
output strings.

* fix(i18n): add English source keys for /curator command

The strict-parity i18n check failed because zh and zh-TW defined the
new /curator command keys while en.js (the source of truth) did not,
producing extra-key parity errors. Add the matching English source
entries so all supported locales share the same key set.

* fix(skills): align curator lifecycle safeguards

* fix(skills): harden curator trust and name guards

* fix(skills): guard curator mutations by workspace trust

* test(skills): cover curator stale-to-active reactivation path

* test(skills): cover curator rollback and restore-collision paths

Add coverage for two previously untested error paths in the auto-skill
curator:
- restoreArchivedAutoSkill refusing to overwrite an existing active
  directory, leaving both the reused directory and the archived copy
  intact.
- runAutoSkillCurator rolling back an archive rename when the post-move
  state persistence fails, returning the skill to the live library and
  leaving nothing stranded in the archive (new isolated test file that
  mocks atomicWriteJSON to fail once).

* test(cli): cover curator command errors and stacked auto-skill usage

- Add mockRejectedValue error-path tests for the /curator status, run,
  restore, and pin commands, asserting each surfaces messageType 'error'
  and that a failed run/restore skips skill-discovery refresh.
- Add positive stacked auto-skill tests to both the non-interactive and
  interactive slash-command paths, asserting recordAutoSkillUsage is
  called once per successful stacked skill carrying project skillDetail.

* fix(skills): reject control-byte auto-skill directory names

isManagedDirectoryName only checked the auto-skill- prefix and basename,
so a crafted directory whose name embeds ANSI/control bytes was treated as
a managed skill and its name printed verbatim by the non-interactive
/curator output (which, unlike the TUI, does not run escapeAnsiCtrlCodes),
enabling terminal control-sequence injection from a cloned repo.

Require the directory name to match SKILL_NAME_PATTERN. A managed dir is
always auto-skill-<name> where <name> passes validateSkillName and the
prefix chars are within the same charset, so this never rejects a
legitimately generated directory (including Unicode skill names, which an
ASCII-only guard would wrongly drop) while excluding ESC/control bytes.

Add a regression test covering a crafted directory with a valid manifest
name so only the directory-name guard can exclude it.

* fix(skills): guard curator state reads and clarify restore errors

Align the curator state read path with the noFollow/lstat guards every
write already uses: refuse a symlinked or non-regular-file state file
(which could otherwise be followed to an external path, /dev/zero, or a
FIFO, causing OOM or a boot hang in untrusted workspaces) and cap the
read size. Also distinguish a present-but-ineligible archived skill from
a missing one in restore error messages.

* fix(skills): close curator TOCTOU reads and preserve seeding baseline

Address review feedback on the auto-skill curator:

- readManagedSkill previously read the manifest via Promise.all([lstat,
  lstat, readFile]); the readFile ran concurrently with the lstat guards,
  so a symlinked SKILL.md pointing at /dev/zero could start an unbounded
  read before the guard rejected it. Read the manifest with O_NOFOLLOW +
  an fstat size bound instead (shared readRegularFileNoFollow helper),
  refusing symlinks atomically and bounding the read.
- readState had the same lstat->readFile TOCTOU window; the O_NOFOLLOW
  read closes it while keeping the existing friendly error messages.
- First-run seeding overwrote firstSeenAt/lastActivityAt with now even
  when recordAutoSkillUsage had already created a record, resetting the
  inactivity clock. Preserve an existing baseline (like useCount/pinned/
  lastUsedAt), while a brand-new skill still gets a fresh now baseline.

Adds regression tests for the seeding-baseline preservation and for
refusing a symlinked manifest.

* fix(core): harden auto-skill curator per review feedback (#7846)

- Resolve node:fs constants lazily so importing the curator does not crash tests that mock node:fs without a constants export.

- Preserve the original error via cause when a rollback also fails.

- Apply the skill-name charset guard to archived directory names reserved in the review-agent task prompt.

* test(core): cover curator restore rollback and re-read guard (#7846)

* fix(core): record auto-skill usage on re-invocation (#7846)

* fix(skills): preserve curator read failures

* fix(skills): harden curator lifecycle guards

* test(skills): cover curator safety paths

* fix(skills): address curator review findings (#7846)

- Ignore future manifest mtimes in lastActivityMs so a bogus timestamp
  cannot make a skill permanently un-curatable
- Skip archived status entries whose directory is also live, preventing
  contradictory double-listing in /curator status
- Check the weekly interval before acquiring the cross-process lock in
  maybeRunAutoSkillCurator so most boots skip the lock entirely
- Use handle.readFile() instead of a single handle.read() to avoid
  silent truncation on short reads

* fix(cli): localize curator messages

* test(skills): cover curator usage safeguards

* fix(skills): address curator review feedback (#7846)

- Isolate per-skill rename failures so a transient error no longer
  aborts the whole pass and prevents state persistence (boot-loop fix)
- Make usage recording fire-and-forget (void instead of await) since
  it is already best-effort and nothing consumes the result
- Skip state file creation when no auto-skills exist
- Prune dead records whose directory exists in neither root
- Sanitize user-supplied directory names in error messages
  (JSON.stringify) to close the ANSI control-sequence echo path
- Split reserved skill names into active/archived lists in the
  review-agent task prompt
- Make collision output actionable with remediation guidance
- Fix rollbackMoves mutating its argument (moved.reverse → copy)
- Add null guard to isMissing for non-object rejections
- Add locale keys for skippedErrors output (9 locales)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: Qwen Autofix <qwen-autofix@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
2026-08-01 07:37:20 +00:00
ChiGao
907c7dea70
fix(cli): stabilize thinking block height, replace transcript overlay with inline Ctrl+O toggle (#8077)
* fix(cli): hide streaming thinking preview, rebind Ctrl+O to inline fullDetail toggle

The streaming thinking block showed a 4-line preview that varied in
height due to empty lines in the model's reasoning output, causing
constant page reflow and flicker during generation.

Changes:
- ThinkBody now renders nothing when collapsed (both streaming and
  committed), keeping the block at a stable 1-line header height.
- Ctrl+O now toggles inline fullDetail mode (like Claude Code): all
  thinking blocks, tool groups, and tool results expand/collapse in
  the main conversation view — no alternate-screen overlay.
- Alt+T preserved as hidden shortcut (same toggle, not shown in UI).
- MainContent passes fullDetail to HistoryItemDisplay via the existing
  ThoughtExpandedContext, so the toggle works in both VP and Static
  rendering paths.
- Removed TranscriptView overlay rendering, transcriptItems memo,
  StreamingContext import, and EMPTY_HISTORY_ITEMS constant.
- Removed dead code: tailVisualLines, grow-only height tracker,
  MAX_STREAMING_THINKING_VISUAL_LINES, openTranscript callback.

* refactor(cli): remove orphaned transcript full-detail infrastructure (#8077)

* fix(cli): update Ctrl+O help text and add thinking-expansion integration test (#8077)

* fix(cli): update docs, help text, and remove transcript dead code (#8077)

* fix(cli): strengthen Ctrl+O full-detail tests and refresh stale docs (#8077)

* fix(cli): address review feedback on test isolation, dead i18n keys, and missing negative case (#8077)

* fix(cli): add M1 mutation-killing assertion to Ctrl+O test (#8077)

The existing test only asserted clearTerminal (a refreshStatic side
effect) and never verified the setThoughtExpanded state flip.  Under
the vi.mock('ink') harness the mocked App never re-renders from a
directly-called handler, so a behavioural allExpanded assertion is
not possible.  Add a structural guard on the handler source that
fails when setThoughtExpanded is removed (mutation M1 verified).

* fix(cli): strengthen Ctrl+O toggle assertion and document non-VP redraw (#8077)

Tighten the structural M1 assertion from .toContain to a regex that
matches the (prev) => !prev updater pattern, catching mutations like
(prev) => true that the old check would miss.  Document the non-VP
scrollback redraw in keyboard-shortcuts.md per maintainer request.

---------

Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-01 02:39:53 +00:00
zhangxy-zju
de022664dc
feat(skills): add disabled skill levels (#8057)
* feat(skills): add disabled skill levels

* fix(core): guard getDisabledSkillLevels for partial Config shims (#8057)

* test(cli): cover disabledLevels safe/bare guard and daemon wiring (#8057)

* fix(cli): guard daemon disabledLevels for safe mode consistency (#8057)

* test(core): add regression guard for missing getDisabledSkillLevels (#8057)

* test(core): pin partial config skill discovery (#8057)

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
2026-08-01 02:22:47 +00:00
destire-mio
412eae24b4
feat(core): add project-level fork profiles (#8148)
* feat(core): add project-level fork profiles

* fix(core): harden fork profile loading

---------

Co-authored-by: destire-mio <248462155+destire-mio@users.noreply.github.com>
2026-08-01 02:20:51 +00:00
destire-mio
4dc50b18e9
feat(memory): protect pinned files during forked Dream (#7714)
* feat(memory): protect pinned files during forked Dream

* fix(memory): harden pinned path protection

* perf(memory): avoid repeated pinned path resolution

* fix(memory): protect pinned memory during extraction

---------

Co-authored-by: destire-mio <248462155+destire-mio@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-01 00:55:45 +00:00
Shaojin Wen
77d8a27eda
feat(daemon): raise default max sessions from 20 to 32 (#8235)
* feat(daemon): raise default max sessions from 20 to 32

* fix(daemon): update test assertion and docs for new default max sessions (32)

* fix(daemon): sync default max sessions

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-31 17:47:51 +00:00
qqqys
7189a68334
fix(serve): isolate managed memory by selected workspace (#8056)
* fix(serve): isolate managed memory by workspace

* feat(serve): add memory project scope option

* test(serve): fix clean build type assertion

* test(serve): cover untrusted workspace memory tasks

* test(memory): cover remaining workspace paths

* test(serve): cover unavailable memory lanes

* test(memory): isolate default scope

* fix(serve): address review feedback on workspace memory isolation (#8056)

- Create secondary ACP mounts on demand for dynamically-registered
  workspaces so the qualified memory routes work beyond boot-time
  runtimes (rename getWorkspaceRememberLane → ensureWorkspaceRememberLane)
- Remove the symlink-alias machinery from getAutoMemoryRoot: it had no
  producer, relaxed a documented invariant, and could throw on the
  per-turn hot path; workspace mode now uses the same plain path.join
  as git-root mode
- Move memoryProjectScope validation into the pre-listen block
- Use the shared sendWorkspaceRuntimeUnavailable helper in server.ts
- Revert an unrelated test mock change; fix misleading 'compatibility
  fallback' wording in ServeOptions
- Add workspace_qualified_memory capability tag, docs for the new flag
  and env var

* docs(serve): add workspace_qualified_memory to conditional features table (#8056)

* fix(serve): address follow-up review feedback on workspace memory isolation (#8056)

* fix(serve): address follow-up review feedback on workspace memory isolation (#8056)

Extract MEMORY_PROJECT_SCOPES const and MemoryProjectScope type in core
so yargs choices, ServeOptions, ServeArgs, and the runQwenServe guard
share one source of truth (reduces drift risk from five to three edit
points; the fast-path guard keeps inline comparisons because an import
boundary test forbids core imports on the lightweight startup path).

Document memory-project-scope caveats in the user-facing docs: daemon
vs standalone CLI split-brain, sanitizeCwd punctuation collisions, and
flag vs env normalization differences. Add the per-lane MAX_PENDING
resource note to the developer configuration reference.

* fix(cli): add missing sessionRuntimeBaseDir to late-add workspace test (#8056)

* test(serve): cover untrusted forget/dream and no-lane memory poll (#8056)

* fix(core): extract MEMORY_PROJECT_SCOPES into zero-import leaf module (#8056)

Importing the constant as a value from the core barrel turned it into a
real static edge that pulled the entire 5.6 MB barrel into the serve
pre-listen bundle closure, breaking the fast-path gate.

Move MEMORY_PROJECT_SCOPES and MemoryProjectScope into a new
memory/scopes.ts with no imports of its own, re-export from paths.ts so
the barrel surface is unchanged, add a ./memoryScopes subpath export,
and switch run-qwen-serve.ts to the narrow import.

Also derive the unknown-scope guard in resolveWorkspaceProjectScope()
from the constant instead of hardcoding 'git-root'.

* test(cli): pin non-allocating contract in workspace memory poll test (#8056)

---------

Co-authored-by: qwen-code-dev-bot <269191875+qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen Autofix <qwen-autofix@users.noreply.github.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-07-31 15:06:38 +00:00
Baby Blue Viper
e3479a6251
docs: worked example for a PreToolUse HTTP hook backed by an external judgment service (#8202)
* docs: worked example for a PreToolUse HTTP hook backed by an external judgment service

The existing remote-security-check config example points at a service
that has to already exist, without showing what that service actually
looks like. Adds a minimal, stdlib-only, runnable adapter (invinoveritas
/review as the judgment source) implementing the exact contract PreToolUse
HTTP hooks expect -- verified live against the real production API, not
just written to look plausible: a genuinely destructive shell command
returns permissionDecision: "deny" with a real explanation, a benign one
returns "allow", and the adapter fails open on any judgment-service-side
error so an outage never blocks legitimate tool calls.

* docs: address review feedback -- disclose affiliation, note swap point clearly

* docs: fix timeout mismatch, add data-handling note, make backend URL configurable, log fail-open state

---------

Co-authored-by: babyblueviper1 <babyblueviper1@users.noreply.github.com>
2026-07-31 13:56:33 +00:00
qwen-code-dev-bot
63780e0d2f
fix(cli): add Option+V for clipboard image paste on macOS (#8120)
* fix(cli): add Option+V for clipboard image paste on macOS (#8118)

macOS terminals intercept Cmd+V for text paste, so the existing
Cmd+V binding for clipboard image paste never fires. Users had to
discover Ctrl+V through trial and error.

Changes:
- keyBindings.ts: add { key: 'v', meta: true } (Option/Alt+V) to
  PASTE_CLIPBOARD_IMAGE bindings on non-Windows platforms
- KeyboardShortcuts.tsx: update macOS hint from 'cmd+v' to
  'ctrl+v / opt+v' to reflect what actually works

* fix(cli): correct macOS image-paste shortcut hint (#8118)

* test(cli): cover platform-specific image-paste shortcut hint (#8118)

* fix(cli): make option+v image paste work on stock macOS terminals (#8118)

* fix(cli): address review feedback on macOS image-paste shortcut (#8118)

- Guard the option-composed glyph lookup with Object.hasOwn so inherited Object.prototype members can never resolve as a composed key.

- Add a non-macOS pass-through test for the "√" glyph, mirroring the existing "†" coverage.

- Correct the keyboard-shortcuts docs: Ctrl+V also pastes on macOS, matching the overlay hint.

---------

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>
2026-07-31 13:42:18 +00:00
sunday
cf547b6a3c
feat(hooks): add SessionDelete event (#8059)
* feat(hooks): add SessionDelete event

* test(hooks): cover SessionDelete failure paths

* fix(hooks): standardize SessionDelete failure handling

* fix(hooks): include session id in SessionDelete hook failure logs (#8059)

* docs(hooks): note that transcript_path is empty over ACP for SessionDelete (#8059)

* refactor(cli): deduplicate SessionDelete hook dispatch (#8059)

* fix(cli): keep SessionDelete hook dispatch ACP-safe

* fix(cli): align SessionDelete short description with other references (#8059)

---------

Co-authored-by: 欢伯 <ri.xur@alibaba-inc.com>
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>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
2026-07-31 13:05:44 +00:00
Dragon
c50137cf86
fix(core): prevent subagents from asking users (#8219) 2026-07-31 12:39:04 +00:00
jinye
08798b4832
feat(core): Normalize tool-call terminal telemetry (#8176)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
* feat(core): normalize tool-call terminal telemetry

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

* fix(desktop): handle cancelled tool telemetry replay

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-31 11:51:14 +00:00
OrbitZore
153d781a34
feat(gitlab-channel): add transient 👀 award emoji while agent is working (#8119)
* feat(gitlab-channel): add transient 👀 award emoji while agent is working

Adds a working-reaction feature to the GitLab channel adapter, mirroring
the GitHub adapter's eyes reaction (PR #8061). When the agent starts
processing a note mention, a 👀 award emoji is added to the note; it is
removed when the run completes, fails, or is cancelled. Both operations
are best-effort and never block the response.

Also replaces the custom Todo interface with gitbeaker's TodoSchema,
introduces GitlabTarget to consolidate target info, and simplifies
processTodo/buildMetadata signatures by passing the parsed target
directly instead of redundant targetType/threadId parameters.

Co-Authored-By: Qwen Code <noreply@alibaba.com>

* fix(gitlab-channel): guard null target and fix reactions leak

Restore null guard for todo.target (dropped in the TodoSchema refactor)
to prevent TypeError when GitLab returns a todo without target. Use
try/finally instead of try/catch for reactions cleanup so entries are
removed on all handleInbound return paths, not just throws.

Co-Authored-By: Qwen Code <noreply@alibaba.com>

* test(gitlab-channel): add poll-driven reaction tests, fix fragile sequencing

Add ReactingGitlabChannel that drives real pollOnce → handleInbound →
onPromptStart/onPromptEnd path, covering #note_ parse, key derivation,
and finally cleanup. Replace the tautological description-mention test
with one that exercises the real isNoteMention guard. Replace bare
Promise.resolve() microtask waits with vi.waitFor in the award-failure
test.

Co-Authored-By: Qwen Code <noreply@alibaba.com>

* test(gitlab-channel): pin double-award guard with dedup test

Add test that calls startPromptForTest twice on the same messageId and
asserts award is called exactly once, pinning the `|| entry.award`
guard in onPromptStart against surviving mutations.

Co-Authored-By: Qwen Code <noreply@alibaba.com>

---------

Co-authored-by: Qwen Code <noreply@alibaba.com>
2026-07-31 10:45:38 +00:00
易良
9a4e924cf1
fix(github-channel): retry definite no-write deliveries (#8087)
* fix(github-channel): retry definite no-write deliveries

* fix(github-channel): preserve concurrent pending deliveries

* fix(github-channel): harden pending retry updates

* fix(github-channel): avoid duplicate pending retries on reconnect

* fix(github-channel): audit recovered deliveries before cleanup

* fix(github-channel): avoid duplicate recovered comments

* fix(github-channel): skip malformed audit entries

* fix(github-channel): bound pending retry recovery

* fix(serve): restore session service import

* fix(serve): remove unused session service import

* fix(github-channel): harden pending delivery recovery

* docs(github): update channel state paths

* fix(github-channel): audit ambiguous pending retries

* fix(github-channel): guard legacy state migration

* fix(github-channel): avoid pending delivery id collisions
2026-07-31 08:06:28 +00:00
destire-mio
079ce5346a
feat(agent): add fork tool execution allowlist (#8066)
* feat(agent): add fork tool execution allowlist

* fix(agent): address fork allowlist review feedback

---------

Co-authored-by: destire-mio <248462155+destire-mio@users.noreply.github.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>
2026-07-30 13:58:51 +00:00
zjgzx1988
953c9d8177
feat(core): tag UserPromptSubmit hook context and record display provenance (#7956)
* 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>
2026-07-30 11:45:23 +00:00
易良
f005f3eee8
feat(github-channel): add transient working reaction (#8061)
* feat(channels): acknowledge GitHub requests

* fix(channels): remove transient GitHub reactions

* test(github-channel): cover reaction failures

* test(github-channel): cover pending reaction dedup

* docs(github-channel): clarify final-only output

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-30 03:00:29 +00:00
Shaojin Wen
0a9db38221
feat(review): add review run — headless review with a machine-readable verdict (#7983)
* 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>
2026-07-30 02:40:10 +00:00
易良
3d5924bd2f
fix(github-channel): validate and document reasonFilter (#8035)
* 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>
2026-07-30 01:17:44 +00:00
Dragon
18cb393e4b
feat(core): preload deferred tools within a context-window threshold (#7922)
* 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>
2026-07-29 23:01:01 +00:00
Nothing Chan
a771e4449e
fix(channels): reject unusable GitHub self-allowlists (#8055) 2026-07-29 15:48:56 +00:00
Mark Xian
c19d321d1f
feat(github-channel): filter notification reasons (#8031) 2026-07-29 15:34:16 +00:00
OrbitZore
ec9c36ef82
feat(channels): add GitLab polling channel adapter (#7862)
* 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>
2026-07-29 14:28:31 +00:00
sunday
aac663f28a
feat(hooks): add security.allowPrivateNetworkHooks to bypass SSRF range checks for trusted scopes (#7968)
* 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>
2026-07-29 13:04:45 +00:00