Commit graph

4164 commits

Author SHA1 Message Date
jinjing.zzj
2b43f0cf54 test(cli): colocate permission-suggestions test with its module
The test lived in src/utils/ and imported ./permission-suggestions.js,
but the implementation is src/nonInteractive/permission-suggestions.ts,
so vitest collection failed with TS2307 and the tests never ran.
Move the test next to the module per house convention; no test-body
changes needed.
2026-08-25 04:29:14 +08:00
yiliang114
adda82a9e7 Merge remote-tracking branch 'origin/main' into HEAD 2026-08-25 00:13:55 +08:00
jinye
1fffa5108d
fix(acp-bridge): Disable permission timeout by default (#9933)
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
npm cache producer / Save npm cache (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
Security Checks / Dependency CVE audit (push) Waiting to run
Security Checks / Secret scan (TruffleHog) (push) Waiting to run
* fix(acp-bridge): disable permission timeout by default

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

* chore: regenerate settings schema

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

* docs(acp-bridge): fix stale timeout comment

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-24 16:08:44 +00:00
Harjoth Khara
95bdd46241
fix(config): accept output.format "stream-json" in the settings schema (#8966)
* fix(config): accept output.format "stream-json" in the settings schema

The runtime already reads and honors output.format: "stream-json" from
settings.json (normalizeOutputFormat -> OutputFormat.STREAM_JSON), and it
is a documented --output-format choice, but the settings schema listed
only text and json. The VS Code companion applies that schema to every
.qwen/settings.json, so it flagged a valid, working config as invalid.

Add stream-json to the source schema and regenerate the shipped
settings.schema.json. Same schema/runtime drift class as #8752.

Closes #8965

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

* fix(config): bind output.format schema values to OutputFormat and document stream-json

Apply the review's non-blocking suggestions:

- Schema options now use the OutputFormat enum constants the runtime's
  normalizeOutputFormat accepts, so the settings schema cannot silently
  drift from core.
- The full enum is pinned in the test (toEqual, sibling-test pattern)
  instead of a toContain probe.
- The format description — schema, regenerated VS Code schema, and the
  settings reference table — now notes that stream-json makes runs
  started with a prompt non-interactive (headless), and the docs table
  lists stream-json as a possible value.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01US2APQw84vvQZZ4pZaKtzn

* test(cli): add OutputFormat to core mock factories that reach settingsSchema

settingsSchema.ts now reads OutputFormat at module load, so the two test
files that mock @qwen-code/qwen-code-core with a hand-built factory and
transitively import it need the enum in the mock, matching how they
already mock ApprovalMode.

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

* fix(config): derive the output.format test pin from the enum and document the argv-only gates

Address the round-2 review:

- The test pins the schema options against Object.values(OutputFormat),
  so a format added in core fails the test until the schema and the
  regenerated JSON follow; the schema comment now states exactly that
  instead of overpromising drift protection from the binding alone.
- The description, regenerated schema, and docs table note that flags
  validated at argv parse time (--include-partial-messages,
  --input-format stream-json) still require the explicit
  --output-format stream-json flag, since those yargs checks run before
  settings are loaded.

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

* test(cli): cover settings-driven stream-json output and name the flag in the docs note

Address the round-3 suggestions: a config test now exercises
output.format stream-json arriving from settings through loadCliConfig,
and the docs table names the --output-format stream-json flag the
argv-time checks require, matching the schema description.

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

* test(cli): pin argv-over-settings output format precedence with differing values

The existing precedence test used the same value on both sides, so an
inverted merge passed the suite. The new case sets settings stream-json
against argv text and asserts text wins.

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

* docs(test): match the enum-pin comment to the order-sensitive assertion

Apply the maintainer review nits: the comment now says array-derived,
order included, which is what toEqual checks, and the precedence test
drops a comment that restated its name.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-24 12:45:51 +00:00
qqqys
24db7f6ef2
feat(review): say when the approach, not the patch, is the open question (#9340)
* feat(review): say when the approach, not the patch, is the open question

Every finding /review emits is anchored to a `file:line` in the current diff.
That is what a finding is — and it means a review can report where an approach
leaks, but never that a different approach would retire all of the leaks at
once.

Measured: one change to `extractAndStripMeta` took three attempts across two
PRs. #9097 (3 rounds, 18 findings) added a timeout to the vm call; #9136 (6
rounds, 56 findings) moved the walk inside the vm and ended up spawning a child
process per call, growing 228 -> 920 source diff lines. #9325 landed it in one
commit by not evaluating the literal at all. All 74 findings were individually
correct, and every one of them went away with the mechanism.

The signal was already there and filed as the wrong kind of thing: `did not
converge within the reverse-audit round cap` appeared four times across the two
PRs, as a coverage gap — "we did not finish looking" — rather than as a
conclusion about the change. Nothing was responsible for reading it as "stop
patching".

Add one advisory paragraph, and one clause on the terminal verdict line, when a
non-Approve round is past the round threshold AND its source diff has grown at
least 3x since the review first measured it. This round's round-cap stop rides
along as corroborating text when present; it is never a trigger on its own.

It is deliberately not a finding. Findings are what the autofix loop consumes,
and that loop patching each finding in turn is the pattern being interrupted —
a finding here would be fixed rather than read. It addresses the human deciding
what happens next, so it is a body paragraph and a verdict-line clause, it adds
no cap, and it never moves the event.

The baseline is a baseline, not the previous round's size: 228 -> 920 across six
rounds is ~1.3x per round, which no per-round delta would notice, but 4.0x
cumulatively. `Ledger.src0` records the first measurement and is carried forward
unchanged, so a diff that later shrinks cannot rewrite its own baseline. It is
the one marker field that survives truncation — the ruling that withholds an
anchor from a partial finding list does not extend to a measurement of the diff.

Known limits, documented rather than papered over: it cannot see across pull
requests, so the three-attempt shape that motivated it would have fired only on
a second forgeable persisted counter; and it is retroactively blank, staying
silent until a PR has posted two rounds after this ships.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(review): suppress approach signal for downgraded approvals

* fix(review): measure approach growth over full diff

* fix(review): validate approach signal evidence

* fix(review): pin approach-signal boundaries and validator coverage

Round-5 review findings: boundary tests for the round threshold,
growth factor, and source-diff floor; the round-cap corroborating
clause and its zh rendering; src0 survival through the pr-context
persist seam and the incremental marker carry-forward; artifact
validator refusal/absence tests for approachSignal; design doc
firing list names the pre-cap verdict.

* fix(review): clamp the approach signal's round at the ledger cap (R9-1)

The signal computed its displayed round with an unclamped `prevRound + 1`
while the ledger marker stamp and the deferred-suggestions clause both
clamp with `Math.min(prevRound + 1, LEDGER_MAX_ROUND)`. `parseLedger`
accepts `round == LEDGER_MAX_ROUND`, so a side file at the cap is
representable and carries forward: one composed body announced
"⚠️ Round 10001" beside a marker stamping `"round":10000`, and the
terminal verdict line printed 10001 too — the doc comment in this same
diff claims all three consumers cannot disagree "at the cap included".

The new test pins the cap for the third consumer, mirroring the existing
deferred-clause cap test; mutation-verified that reverting the clamp
turns it red with `round: 10001`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: qqqys <266654365+qqqys@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>
2026-08-24 12:27:26 +00:00
Shaojin Wen
d1cfd87683
feat(review): promote language-pitfall and wrapper/proxy checks out of Agent 1a (#9805)
* feat(review): promote language-pitfall and wrapper/proxy checks out of Agent 1a (#9788)

Split the two checks folded into Agent 1a's line-by-line brief into dedicated
Step 3A roles at high effort: Agent 1d (language-pitfall scan, always) and
Agent 1e (wrapper/proxy routing, rostered when the plan's wrapperSignal is
true — a capture-time vocabulary heuristic that fails safe: only an explicit
false keeps it out, so version-skewed plans still owe the check). The roster,
check-coverage and agent-prompt all read the gate from the plan, so a run that
skips either agent is named. Briefs, SKILL.md, and the user-facing code-review
doc updated; 1a keeps its walk minus the two clauses.

* fix(review): address round-1 feedback on the 1d/1e split (#9805)

* fix(review): address round-2 feedback on the 1d/1e split (#9805)

* fix(review): address round-3 feedback on the 1d/1e split (#9805)

* fix(review): address round-4 feedback on the 1d/1e split (#9805)

* fix(review): address round-5 feedback on the 1d/1e split (#9805)

---------

Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
2026-08-24 11:15:58 +00:00
顾盼
37cedea5b2
feat(computer-use): replace built-in tools with bundled skill (#9856) 2026-08-24 11:05:23 +00:00
jinye
b5aec6691e
fix(serve): Canonicalize Live task bridge session IDs (#9819)
* fix(serve): Canonicalize Live task bridge session IDs

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

* test(serve): Pin canonical Live owner lookup

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

* fix(cli): harden mixed-case live session routing

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-24 09:11:38 +00:00
Dragon
43d46be912
refactor(core): shrink the content generator interface (#9676)
* refactor(core): shrink content generator interface

* refactor(core): remove orphaned request-tokenizer estimator cluster

Removing countTokens from both providers deleted the last production
consumers of RequestTokenEstimator. Delete the orphaned cluster:
requestTokenizer.ts (330), imageTokenizer.ts (534), types.ts (36), the
directory barrel (11), and both test files (608 lines). Also drop the
inert vi.mock of requestTokenizer.js left in client.test.ts and the
stale dimension-extractor cross-reference in review/lib/assets.ts.

textTokenizer.ts and supportedImageFormats.ts stay: converter.ts, pdf.ts,
and fileUtils.ts still consume them and the core barrel re-exports them.

* docs(design): sync lazy-google-genai-loading record with shrunk interface

countTokens and useSummarizedThinking no longer exist on ContentGenerator,
so the design record for the lazy-wrapper architecture must not keep
advertising them: list the three remaining shared async operations, drop
the useSummarizedThinking sentence and the summarized-thinking item from
the consumer audit and Verification section, and add a dated note
recording the interface shrink from PR #9676.

* ci: record cd-cua-driver.yml size growth in .size-baseline

Same latent main-side violation as fixed in #9682: #9587 grew the
workflow without a baseline update; record the new size as the check
message directs (precedent #9747).

* docs: finish scrubbing tokenizer references after estimator-cluster removal

Follow-up to 0ee17632c7/1871bb5b81 (review round 2):
- supportedImageFormats.ts header and getSupportedImageFormatsString doc
  no longer describe a tokenizer decode/metadata-extraction stage; the
  list is now documented as the vision-input acceptance list, with token
  accounting noted as the flat DEFAULT_IMAGE_TOKEN_ESTIMATE.
- web-shell-image-drag-and-drop.md's BMP rationale no longer claims
  ImageTokenizer parses BMP dimensions; dated sync note added stating
  BMP support rests on SUPPORTED_IMAGE_MIME_TYPES plus converter
  passthrough since PR #9676.

* docs: drop tokenizer from the BMP test-plan line

Follow-up to 18f08c0924: the test plan still required converter/tokenizer
focused tests for image paths; the image-tokenizer estimator cluster was
removed in PR #9676 (text tokenizer is unaffected and out of scope here).
2026-08-24 08:30:18 +00:00
易良
27285a5243
refactor: centralize approval mode contracts (#9796)
* refactor: centralize approval mode contracts

* fix: align Python SDK import grouping

* test: restore approval mode exports in CLI mocks

* fix: close approval mode drift gaps

* test(cli): preserve core exports in serve mocks

* fix: close approval mode review gaps

* test(cli): complete permission request fixture

* test(sdk): match approval mode route

* test(approval): close review coverage gaps

* test(sdk): cover approval mode global scope path
2026-08-24 07:46:50 +00:00
易良
9ef32c5f3c
fix(core): make team shutdown a leader-only tool (#9401)
* fix(core): make team shutdown a leader-only tool

send_message carried an optional single-value enum `type:
['shutdown_request']` described as "structured message type for control
flow". Models filled it while composing an ordinary report; the call was
then rejected leader-only and the report content was discarded, leaving
the teammate retrying a report the leader never received.

Split control from content at the tool boundary rather than validating
the field harder. `type` is removed from send_message entirely, and
shutdown becomes request_shutdown, which createToolRegistry skips for
subagent-context registries — so a teammate has no declaration for it and
cannot emit the call at all, instead of emitting one and being refused.

The mailbox wire format is unchanged: sendStructuredMessage still writes
`type: 'shutdown_request'` with `from: LEADER_NAME`. Only the tool
surface moved.

Fixes #9276

* fix(ui): cover request_shutdown in tool display-name drift guards

The new leader-only request_shutdown tool landed without entries in the
drift-guarded display surfaces, breaking CI:

- web-shell TOOL_DISPLAY_NAMES (toolFormatting.drift.test.ts) and the
  zh badge label in client/i18n.tsx (toolFormatting.test.ts parity)
- cli toolDisplayName locale entries enforced by i18n/index.test.ts and
  check-i18n key parity (en identity / zh / zh-TW)

* fix(core): close the leader-only hole the review found, and register the display name

Three criticals from review, all real.

The leader-only guarantee did not hold. "Enforced by absence" covers a
registry that was *built* with forSubAgent, but runSingleDispatch's workflow
fast path hands a subagent the parent leader's registry untouched — and that
one does contain request_shutdown. So a workflow subagent could request a
shutdown as the leader. The runtime guard now uses
isSubagentLikeExecutionContext rather than isTeammate, which covers every
subagent-like context instead of only a teammate identity.

RequestShutdown had no toolDisplayName locale entry, failing the CLI i18n
guard, and no entry in web-shell's manually synced TOOL_DISPLAY_NAMES, failing
that package's drift guard. Both added, matching the locales that carry
SendMessage.

And the suggestions: a config test asserting the tool is present in a leader
registry and absent from a forSubAgent one — the property everything rests on,
which nothing checked; the deliberate 'ask' permission default pinned; the
empty and whitespace recipient guard covered, since the schema admits both;
and the dead requestShutdown mock left in send-message.test.ts's helper type
removed.

Verified: 534 config tests, 30 across request-shutdown and send-message.

* fix(core): exclude request_shutdown from subagent and teammate tool sets

* fix(i18n): translate the RequestShutdown tool display name in Catalan

The entry was left as raw English while every sibling team tool is
translated.

---------

Co-authored-by: yiliang114 <jinjing.zzj@gmail.com>
2026-08-24 07:36:18 +00:00
jinye
b2d0687213
feat(serve): add --open-with-auth (#9738)
* docs(serve): propose ephemeral auth for --open

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

* docs(serve): address ephemeral auth review

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

* docs(serve): clarify asset pre-check boundary

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

* docs(serve): centralize token selection plan

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

* docs(serve): make ephemeral auth opt in

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

* docs(serve): align ineligible-browser handling with manual-URL fallback

Browser-launch eligibility is a heuristic with common false negatives,
so it is no longer a hard pre-listen gate: an ineligible environment
warns (naming the tripped signal), starts the daemon, and prints the
fragment-bearing manual URL, matching the launch-failure recovery.
Also pin the generation breadcrumb with planned test assertions.

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

* feat(serve): add opt-in ephemeral auth for --open

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

* feat(serve): replace ephemeral auth with --open-with-auth

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

* docs(serve): clarify temporary token storage

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

* docs(serve): clarify ephemeral token persistence

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

* codex: address PR review feedback (#9738)

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

* codex: address PR review feedback (#9738)

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-24 07:33:05 +00:00
qqqys
c7c4dc80e0
feat(core): add the output-style layer to the system prompt (#9565)
* feat(core): add the output-style layer to the system prompt

Introduces output styles: a named prompt section that changes how the
agent reports its work, selected per session. This is the core half —
the registry, the four built-in styles, and the prompt wiring. Nothing
selects a style yet; the picker, the settings key, and the per-turn
reminder follow separately.

The style lands at the end of the stable `base` layer: after the mandates
it refines, and still ahead of every context/volatile layer, so the
prompt prefix stays cacheable for the whole session.

Two precedence rules, both following what the file already does for
QWEN_SYSTEM_IDENTITY_MD:

- A QWEN_SYSTEM_MD override wins. That file is a full, user-owned prompt;
  layering our section onto it would defeat the override.
- The QWEN_WRITE_SYSTEM_MD dump stays a pure base prompt. A style sits on
  top of a base, so baking it into the dump would apply it twice once
  that file is fed back through QWEN_SYSTEM_MD.

`keepCodingInstructions: false` lets a style replace the base outright,
for styles that are not about software engineering at all. All four
built-ins keep it true — Proactive in particular changes how much you
plan and ask, not what you are allowed to do, and says so in its prompt
so it does not read as a second permission knob next to ApprovalMode.

Subagents and arena runs deliberately do not inherit the main session's
style: it would multiply Explanatory's insight blocks across every child,
and skew an arena comparison that is supposed to isolate the model.

* fix(core): narrow keepCodingInstructions to the section it names

`keepCodingInstructions: false` replaced the whole base prompt, which
took the safety rules down with the workflow guidance: a non-coding
style lost `# Executing actions with care`, the mandates, the tool
guidance and the tone section along with the part it meant to drop.

It now omits exactly one section — the software-engineering workflow
guidance, split out as `getSoftwareEngineeringTasksSection()`. Every
other section stays under every style. A style adjusts how work is
reported; it never switches off the rules for taking risky actions.

Two smaller corrections in the same area:

The identity sentence now points at the style when one is active
("responding according to your Output Style below") instead of claiming
the agent specializes in software engineering under a style that says
otherwise. A `QWEN_SYSTEM_IDENTITY_MD` override is still inserted
verbatim — that wording is distributor-owned and not ours to rewrite.

The style section is headed `# Output Style: <name>` rather than
`# <name> Style Active`. The heading is the contract a custom style file
will rely on once user and project styles load: the file body becomes
the prompt verbatim and the heading is what names it.

The per-turn reminder is now every style's, not just the two that spell
one out. `turnReminder` overrides the generic wording rather than
deciding whether a reminder exists at all — an Explanatory session
drifts back to terse answers as readily as a Concise one does.
`getOutputStyleTurnReminder()` renders the line the injection site will
use.

Because the style now feeds the base prompt rather than only being
appended to it, the QWEN_WRITE_SYSTEM_MD dump builds its own unstyled
copy, preserving the invariant that the dump is a reusable base.

The prompts.test.ts snapshots are unchanged, which is the evidence that
a session with no style selected still gets a byte-identical prompt.

* test(core): pin the output-style layer where review round 1 found it unpinned

Two coverage gaps from the r1 review, both confirmed by mutation probes:

R1-1 (prompts.test.ts): the `QWEN_SYSTEM_IDENTITY_MD` override combined with
an active output style was pinned by nothing — the identity-override tests
pass no style and the `outputStyle parameter` tests never stub the override.
Skipping the style append under an override kept all 105 tests green while
silently dropping the user's style for every deployment shipping an identity
override. The new case asserts the override text wins verbatim, the styled
identity sentence is skipped, and `# Output Style: Concise` still lands.

R1-2 (contextCommand.test.ts): the `config.getOutputStyle()` forwarding into
`getCoreSystemPrompt` was never exercised with a style active — every config
mock returned `undefined`. Deleting the argument kept the suite at 16/16
while `/context` undercounted system-prompt tokens by the style section. The
new case bills a `Concise` style and asserts the estimate grows by that
section.

Mutation verification: skipping the style append under an identity override
fails only the new prompts case (1 failed | 105 passed); deleting the
`config.getOutputStyle()` argument fails only the new contextCommand case
(1 failed | 16 passed).

* fix(core): align output style prompt consumers

* test(core): pin Learning output style modes

* test(core): pin Learning style in ACP prompts

* fix(core): align output-style doc comment and Concise description

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-24 07:19:57 +00:00
Stellar鱼
f241c19ace
fix(core): support per-provider stream idle timeout (#9795) 2026-08-24 06:46:25 +00:00
ShiZai
4f53c7d160
fix(core): route debug logs through sessionIdContext before global session (#9538)
* fix(core): route debug logs through sessionIdContext before global session

In daemon/ACP mode one process hosts many sessions. Config creation
overwrites the process-wide debug log session, so logs for session A
were written to session B's file after B's Config was created.

ACP sessions already wrap execution in sessionIdContext.run(sessionId).
Make debugLogger.getActiveSession() consult that context before falling
back to the global session, while keeping runWithDebugLogSession /
runWithoutDebugLogSession overrides first.

Fixes #9535.

* fix(core): bind sessionIdContext in ACP control dispatch and refresh latest alias

- Wrap AcpAgent.extMethod dispatch in sessionIdContext.run(sessionId, ...)
  when a sessionId parameter is present, so control-plane handlers like
  qwen/control/session/recap route debug logs to the targeted session.
- Make debugLogger.getActiveSession() consult sessionIdContext before
  falling back to the process-wide session.
- Refresh the 'latest' debug-log alias when the active writing session
  changes, so multi-session daemons don't leave it stale.
- Add regression tests for control-plane dispatch binding and alias
  refresh.

Relates to #9535.

* refactor(core): harden debug logger alias and ACP session context binding

- Gate AcpAgent.extMethod sessionIdContext.run on this.sessions.has()
  to avoid binding unsanitized caller-supplied strings.
- Serialize latest-debug-alias updates via a module-level promise chain
  and key the dedup marker by debug-directory + sessionId.
- Extract doUpdateLatestDebugLogAlias so marker/alias bookkeeping is
  centralized in updateLatestDebugLogAlias.
- Strengthen regression tests: assert the run callback actually
  dispatches, and add a negative case for alias dedup.

Relates to #9535.

* fix(cli): use Config session id for async-context binding and cover review findings

- Bind sessionIdContext to session.getConfig().getSessionId() in AcpAgent.extMethod
  so the context spelling matches Session.ts and avoids splitting legacy sessions
  across upper/lower-case debug log files (R3-1).
- Wrap top-level session handlers (cancel, setSessionMode, unstable_setSessionModel,
  setSessionConfigOption) with sessionIdContext.run (R2-4).
- Only bind extMethod dispatch for session-scoped methods, preventing global
  operations from being framed with a leaked session context (R4-1).
- Add regression tests for Config-spelling binding, global-method allowlist, and
  serialized latest-alias updates (R3-4).

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

---------

Co-authored-by: tomsen-ai <230283659+tomsen-ai@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 05:51:24 +00:00
C0d3N1nja97342
aac9606f78
fix(cli): skip terminal redraw optimizer on WSL/ConPTY (#7897)
* fix(cli): skip terminal redraw optimizer on WSL/ConPTY and enable sync output on Windows Terminal (#7634)

The streaming text repetition bug on WSL + Windows Terminal is caused by
the terminal redraw optimizer batching cursor-up sequences, which ConPTY
processes differently from individual per-line erases. The cursor lands
at the wrong row, causing each new frame to overlap remnants of the
previous one.

Two fixes:
1. Skip the redraw optimizer when WSL (WSL_DISTRO_NAME / WSL_INTEROP)
   or Windows Terminal (WT_SESSION) is detected, falling back to Ink's
   original per-line erase sequences that ConPTY handles correctly.
2. Enable synchronized output (DEC mode 2026) for Windows Terminal,
   which has supported it since v1.6, making frame updates atomic and
   masking any residual cursor positioning issues.

Fixes #7634

* fix: add WSL_INTEROP test and clear env vars in beforeEach to fix test fragility

Address review feedback on #7897:
- P1: clear WSL/Windows Terminal env vars in beforeEach so existing
  optimizer tests don't silently break when run inside WSL
- P2: add dedicated test for WSL_INTEROP detection

* review: address wenshao feedback on #7897

- Accept injectable env in installTerminalRedrawOptimizer (matches sibling
  terminalSupportsSynchronizedOutput), eliminating the need for
  beforeEach env-stubbing in test files
- Add QWEN_CODE_LEGACY_ERASE_LINES=0 as a force-on escape hatch for
  WSL/Windows Terminal users whose terminals handle the batched
  sequences correctly
- Collapse three near-identical WSL/WT skip tests into it.each
- Correct Windows Terminal DEC 2026 support version: v1.18, not v1.6
- Move WT_SESSION check above the TERM declaration in
  terminalSupportsSynchronizedOutput so the term isn't declared before
  its only consumer
- Add a table case asserting TMUX guard still wins over WT_SESSION
- Pass explicit empty env to installTerminalRedrawOptimizer in the
  synchronizedOutput composition test so it doesn't depend on the
  runner's environment

* fix(cli): narrow optimizer skip to WSL only, drop WT_SESSION

Per wenshao's review: WT_SESSION is set on the Windows side and is not
propagated into WSL shells without WSLENV, so it can never be the env
var that fires for #7634. Remove WT_SESSION from the optimizer skip
(WSL_DISTRO_NAME + WSL_INTEROP remain) and from the synchronized-output
allowlist. The synchronized-output change for Windows Terminal belongs
in its own PR once confirmed; bundling it into a WSL bug fix mixed two
independent behavior changes.

Also correct the comment: 'WSL or Windows Terminal' -> 'WSSL only',
and remove the WT_SESSION test cases from both test files.

* fix(cli): clean up WT_SESSION comment residue and pin its exclusion

Per review: the drop of the WT_SESSION skip left stale comments and no
test pinning the deliberate exclusion. Fix the force-enable comment
(WSL only, not Windows Terminal), complete the truncated WT_SESSION
rationale, and add a test asserting WT_SESSION alone does NOT trigger
the skip (it is not propagated into WSL shells). Also stub
QWEN_CODE_LEGACY_ERASE_LINES in beforeEach so the suite is isolated
from a host that has the flag set.

* refactor(cli): extract shared isWsl(env) into terminal-env util

WSL detection was inlined in terminalRedrawOptimizer (this PR) and
duplicated as a private helper in voice-availability. Extract a single
isWsl(env) into ui/utils/terminal-env.ts and use it from both sites so
the marker set cannot drift. Requested by maintainer in #7897 reviews.

* fix(ui): add license header and gate WSL_INTEROP in voice preflight

Round-3 review: terminal-env.ts shipped without the @license header
every sibling carries; and the voice-side isWsl migration was inert under
the test probe because voice-availability.test.ts only exercised the
WSL_DISTRO_NAME marker. Add the header and cover WSL_INTEROP via it.each. #7897

* docs(ui): note the separate core-side WSL check in terminal-env

Round-4 review: the extraction comment claimed the marker set cannot
drift, but ripgrepUtils.wslTimeout() in packages/core keeps its own
narrower WSL_INTEROP-only check because core cannot import from cli.
Document the exception so a future maintainer greps both sites. #7897

* docs(cli): document QWEN_CODE_LEGACY_ERASE_LINES escape hatch

Round-5 review (R5-1): isWsl(env) relies solely on env markers, which
env-scrubbing launchers (sudo, env -i) strip - so the #7634 skip never
fires in those contexts. Document the launch-time =1 fallback and note
it must be passed at launch because sudo drops the flag too. Also closes
the round-2 R2-3 gap (the flag was previously undocumented). #7897

* refactor(cli): move isWsl to core and apply maintainer review polish

wenshao's manual review suggested moving the shared WSL marker check to
packages/core so cli can import it (core cannot import from cli), while
ripgrepUtils.wslTimeout() keeps its deliberately narrower predicate. Also:

- Sharpen the ConPTY divergence comment with the concrete sequences the
  optimizer emits (CSI 1 B cursor-down, CSI n A multi-count) that Ink's
  native erase path never does.
- Replace the beforeEach vi.stubEnv test fixture with explicit empty-env
  arguments (truer 'not on WSL' fixture, no host-env dependency).
- Note the env parameter exists for testability.
- Trim the moved file's doc block to durable facts and tighten the docs
  row wording. #7897

* test(cli): pin the env default-parameter seam in redraw optimizer

Round-7 review: the production call path (installTerminalRedrawOptimizer
with no env arg) was never exercised - every test passed env explicitly,
so a mutation to the = process.env default (e.g. = {}) would pass green
while silently disabling the WSL skip and =1 escape hatch in production.
Add a hermetic test that stubs WSL_DISTRO_NAME and asserts the no-arg call
skips the optimizer. #7897

* test(cli): restore afterEach env cleanup for default-seam test

Round-8 review: placing vi.unstubAllEnvs() as the last statement in the
default-seam test body meant a failing expect (the exact regression the
test pins) would skip the cleanup and leak WSL_DISTRO_NAME=Ubuntu into
process.env for the rest of the file. Move the cleanup back into the
describe-level afterEach so it runs even on assertion failure. #7897

* test(cli): close the two minor coverage gaps from chiga0's review

Maintainer chiga0 approved the PR but noted two minor test gaps:

- The default-seam test only stubbed WSL_DISTRO_NAME, so a host-set
  QWEN_CODE_LEGACY_ERASE_LINES=1 would pass it for the wrong reason; stub
  the flag too so the assertion depends only on the WSL marker.
- The tri-state flag has no case for a non-standard truthy value; add one
  pinning that 'garbage' falls through to the platform default (WSL skip). #7897

* docs(cli): correct 'only path' claim in ConPTY divergence comment

Round-11 review: the comment asserted the optimizer is the ONLY path
emitting CSI 1 B / CSI n A, but the repo's patched ink build also emits
both sequence classes on its cursor-positioning path (buildCursorSuffix /
buildReturnToBottom, reachable via BaseTextInput.setCursorPosition).
Skipping the optimizer on WSL does not remove these from interactive
input. Narrow the claim to the per-frame erase-and-redraw path. #7897
2026-08-24 03:58:48 +00:00
Shaojin Wen
03bcfe44b5
refactor(review): split SKILL.md into a core body plus verdict-gated reference files (#9804)
* refactor(review): split SKILL.md into core body plus verdict-gated references (#9787)

The bundled review skill's SKILL.md (381,984 bytes, ~95k tokens) was
injected whole on every run, although large stretches are conditional
territory a given run never touches. Split it into a core body plus
reference files the orchestrator reads on demand, gated on the parse-args
verdict it already holds:

- references/posting.md — whole Step 7 (authorisation gate, presubmit,
  anchors, submit, 422/head-drift recovery, publish-assets). Loaded only
  when posting is live (comment.effective or a same-session post request;
  PR + high only). Its compose-state field list relocates verbatim to
  Step 6's Verdict section, because a report-only run still writes that
  state for compose-review without ever loading posting.md.
- references/persistence.md — whole Step 8 (tail batching, report,
  artifact registration, incremental cache). Loaded before Step 8 on every
  run except cross-repo lightweight mode.
- references/aone.md — the self-contained Aone blocks of Step 1 (clone and
  two-host rules, a1-backed surface, the five submit failure shapes, dedup
  shape notes). Loaded before match-remote when the host/meta says Aone.

The split moves whole steps; incident-backed rules stay with the step they
guard. The write prohibition and the posting gates remain in the injected
core so they bind runs that never load a file. No enterprise.md: the GHE
host notes are sentences woven into universal paragraphs, and extracting
them would strip rules from steps that remain in core.

Injected prompt: 381,125 -> 304,427 body bytes. Typical non-posting runs
(local/file/PR, any effort) save ~58 KB (~15%); lightweight runs ~77 KB
(~20%); posting runs load posting.md back and save only the Aone block.
The issue's "roughly a third" estimate is unreachable under its own
whole-step guardrail — Steps 1 and 6 dominate the core and interleaving
forbids fragmenting them; Step 5 / Step 3C effort-gated splits are the
natural follow-up.

Drive-by, verified against #9627's revert-guard test and the a1
implementation: three stale sentences still claiming comment-status "has
no Aone backing" are aligned with the a1-backed behavior that landed in
#9627.

Tests: SKILL.test.ts revert guards now govern the full corpus (SKILL.md +
references), with new pins for the gates, the core-retained invariants and
the no-duplication invariant; run-skill-parity reads the corpus oracle;
bundled-skills integration pins the shipped reference files. Verified by
build + bundle, all review-skill suites, and a real-model E2E run of the
split skill (verdict-gated reads observed: persistence.md loaded before
Step 8, posting.md and aone.md correctly skipped).

* fix(review): drop uninterpolated template tokens from skill references (#9804)

* test(review): guard stems oracle by persistence.md, pin gate clauses to bullets (#9804)

* fix(review): close Step 7 reference doc gaps from reverse audit (#9804)

---------

Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
2026-08-24 02:41:49 +00:00
qqqys
75fb40d832
refactor(goal): render Goal continuation prompts from one core renderer (#9581)
* refactor(goal): render Goal continuation prompts from one core renderer

The prompt sent when `runtime.finishTurn` schedules another Goal turn was
assembled independently in three hosts: the TUI's inline array in
`useGeminiStream`, and a `buildGoalContinuationParts` in each of the ACP
session and the non-interactive CLI. Three copies of the same four shared
lines have already drifted -- the TUI carries the anti-spoofing guard lines
but no objective, while ACP and non-interactive carry the runtime
continuation context but no guard lines.

Upcoming work adds further variants (an "objective was edited" announcement
and a budget wind-down prompt). With the text living in three places, every
new variant means three edits, which is precisely how the current drift was
produced. This moves assembly into `packages/core/src/goals/goal-continuation-prompt.ts`,
where a variant is a case in one function and the shared prefix exists once.
The two `buildGoalContinuationParts` helpers keep their names and signatures
and simply delegate.

This is a pure refactor: no prompt text changes. Each host still emits a
byte-identical string to the one it emitted before. The existing drift is
preserved deliberately and is left for a separate, behavior-changing
follow-up. The new unit test pins the complete rendered string for both
variants with and without verifier feedback, so any future edit to a line
surfaces as a test diff; the existing host tests pass unmodified.

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

* fix(goal): tighten continuation renderer contract

* test(goal): cover verifier feedback hosts

* refactor(goal): hoist Goal continuation parts builder into core (#9581)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-24 02:36:02 +00:00
Shaojin Wen
20e076f8f3
fix(core): surface nested sub-agent approvals under background parents (#9793)
* fix(core): surface nested sub-agent approvals under background parents

A tool call needing confirmation inside a nested sub-agent (launched by a
background agent or fork) was neither surfaced nor denied: the prompt-
avoidance policy was stamped on an Object.create wrapper while the rebuilt
tool registry binds to agentConfig, so nested schedulers resolved
Config.prototype's false and believed they could prompt; and the nested
invocation's emitter was never bridged, so TOOL_WAITING_APPROVAL fired with
no listener and the call waited forever - the enclosing agent hung silently.

- Stamp getShouldAvoidPermissionPrompts on agentConfig itself (launch and
  resume paths) so nested launches inherit the real policy through their
  config prototype chains: hang-forever becomes an explicit deny when
  bubbling is off.
- Bridge nested foreground launches' approval events onto the nearest
  backgrounded running ancestor's Background-tasks entry (walking the
  registry's parentAgentId lineage), so they park where the ancestor's own
  approvals go and the user can answer them from the dialog.
- Mark bridged approvals with the nested runtime's subagentId (declared by
  the bridge caller via nestedSource - runtime ids and registry ids use
  different suffixes so comparing them cannot work) and show the waiter in
  the Background tasks dialog.

Fixes #9782

* chore(i18n): add 'from nested agent' locale entries

* fix(core): harden nested sub-agent approval bridging (#9793)

* fix(core): tighten nested approval bridging per review round 2 (#9793)

- addPendingApproval returns a discriminated result ('parked' |
  'duplicate' | 'unavailable') mirroring the workflow registry's
  parkPendingApproval, so the bridge drops re-emitted events without
  re-running the dedup scan on the caller side
- log a debug line when a re-emitted approval event is dropped, so
  "approval never appeared" sessions can tell a drop from a lost event
- extract stampBackgroundPromptPolicy next to createApprovalModeOverride;
  both the launch and resume paths now share one stamp + rationale
- gate the nested approval bridge on the inherited prompt-avoidance
  policy like the sibling bridges, instead of wiring a dead subscription
  under auto-denying ancestors
- refresh two JSDoc examples that still cited the deleted
  Object.create wrapper

* fix(core): key parked background approvals on runtime identity (#9793)

Generated tool-call ids (`call_qwen_N`) are only unique per conversation,
so two nested sub-agents whose first id-less call needs confirmation both
arrive under the same callId on their shared background ancestor entry.
The parked-approval queue keyed identity on callId alone: the second
runtime's prompt was dropped as a "duplicate" with nothing in the dialog
to answer, and a resolution or TOOL_RESULT for one runtime could clear
another runtime's parked prompt.

Key identity on (subagentId, callId) — the same composite the workflow
run registry already uses — across addPendingApproval's duplicate
predicate, resolvePendingApproval, and clearPendingApproval. Own
approvals stay unstamped, so same-call re-emissions from the entry's own
runtime still dedupe while different runtimes never collide.

* fix(core): attribute nested approval logs and pin composite-key dedup (#9793)

* fix(core): attribute remaining approval failures and pin own-vs-nested collision tests (#9793)

---------

Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
2026-08-24 02:31:45 +00:00
Shaojin Wen
95b2cf4cb1
fix(review): pin the verified git identity across the residue probe (#9557) (#9742)
* fix(review): pin the verified git identity across the residue probe (#9557)

`worktreeResidue` verified a tree's identity once and then ran five commands
that each re-discovered the repository from `cwd`, through the same `.git`
file the check had read — a file writable by anything running as this user.
Two halves, and they close different windows:

- **The gate now requires the gitfile's admin entry to point BACK at this
  tree.** A repository whose `core.worktree` names this path answers
  `--show-toplevel` with this path, so the old gate saw itself while every
  command after it measured the plant's index — which already holds the
  contamination as committed content, so `status` comes back clean for a tree
  with a mutant in it. A planted standalone repo has no admin entry to
  round-trip; `scratch-tree` gates its own reset on exactly this.
- **The verified `--git-dir`/`--work-tree` are pinned onto every later spawn**,
  so a swap that lands AFTER the gate cannot redirect them either.

The reason recorded against doing this — that an explicit pin changes what
`ls-files` resolves, so it needs its own round — was measured and is wrong.
Across a standalone checkout, a linked worktree, a superproject with an
initialised submodule, and a worktree reached through a symlinked ancestor, all
five commands return **byte-identical** output pinned and unpinned. What was
right about it is that it needed measuring, which is what this is.

The backpointer half is pinned by a test that goes red without it. The pin half
is not: it closes a check-then-use window the suite cannot open deterministically
(the swap has to land between two spawns). Its evidence is a shell repro — with
the identity captured, then the gitfile swapped, discovery reports a clean tree
while the pinned commands still report ` M a.ts` and the untracked probe file.
Saying that rather than implying a pin that is not there.

* fix(review): refuse symlink redirects and forged admin entries in the residue probe (#9742)

* fix(review): fail closed on unanchored clean verdicts and steered boundaries (#9742)

The residue probe's identity gate answered CLEAN for shapes it cannot
vouch for. Four closes, all fail-closed:

- A repository answering for a path it does not contain — the common
  dir no literal ancestor of the tree path — is refused before the
  symlink walk. That is the shape a forge uses to steer the walk's own
  stop boundary so it fires before the planted link is lstat'd, and the
  walk's bound escaping to the filesystem root misdiagnosed refusals
  (and reddened the suite on hosts whose tmpdir resolves through a
  symlink).
- A clean measurement no fetched-head record anchored is refused: a
  forged pair answers clean too, so an unanchored empty status
  certifies whichever index the gitfile names. Dirty reports still
  name paths — a forge answers clean, never dirty.
- The agent-prompt wiring fails closed when the plan's fetchedSha is
  absent or malformed instead of degrading to an unpinned probe: every
  worktree-mode fetch writes the field.
- A dangling admin-entry backpointer is "does not point back", not the
  outer catch's "not a git worktree".

Comments now say what the pin actually closes (post-gate identity
redirect, pre-planted shapes) and what it does not (in-window state
presentation by an active writer — cost-raising, not closure; the
structural close is the sandbox boundary, #9556).

Witnessed: every guard flips its focused tests when removed (mutation
probes), including new ones for the case-fold, the unreadable-HEAD arm,
the sha handover seam, and the sha-less production caller. Fixtures
realpath their roots so the suite holds under any tmpdir spelling.

* fix(review): anchor the scratch-tree residue check and name unmeasured reasons (#9742)

Three closes on the round-4 findings, all inside the residue probe's
own footprint:

- The sha-less production caller flattened the tampering signal the
  probe exists to protect: the no-record refusal fired on 100% of
  clean runs, so a note that fires always is a note nobody reads.
  scratch-tree now takes --fetched-sha, agent-prompt welds the plan's
  record into the verifier's command, and a healthy shared tree
  measures clean again — a forged pair is refused at the pin with a
  distinguishable reason instead.
- The unmeasured renderers blamed `git status` for refusals where it
  never failed (the gates run before it; the no-record tail runs after
  it answered CLEAN), sending triagers to debug a git environment
  with nothing wrong. All three renderings now name a reason.
- The ancestor-symlink walk had no witness: both sibling shapes
  refuse at earlier gates, and deleting the arm shipped green
  (measured). A shape that passes the leaf, the self-equality and the
  boundary gate leaves only the walk to refuse it.

* test(review): pin the fetched-sha CLI contract and the weld continuation (#9742)

* test(review): share the forge-territory fixture across the redirect residue tests (#9742)

* fix(review): admit healthy trees spelled through links above the root (#9742)

* fix(review): canonicalise both spellings the containment gate compares (#9742)

* fix(review): admit healthy layouts and refuse unpinned or drifting trees (#9742)

---------

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-24 02:30:09 +00:00
易良
5cb946b55b
feat(scheduled-tasks): allow creating a task with an existing session (#9361)
* feat(scheduled-tasks): allow creating a task with an existing session

POST /scheduled-tasks and the workspace-qualified endpoint now accept an
optional `sessionId`. When provided, the task binds to that existing
session instead of minting a dedicated one. The session is validated up
front: it must be live in the target workspace, idle, not archived, and
not already bound to another scheduled task (checked both in a
best-effort pre-read and authoritatively under the cron write lock).

A failed create never tears down a caller-provided session (only
route-minted sessions roll back); after a successful create the session
follows the regular scheduled-task session lifecycle. Omitting
`sessionId` keeps the dedicated-session behavior unchanged.

Closes #8906

* fix(serve): harden scheduled-task session binding per bot review round 1

Four fixes inside the route, each pinned by a test:

- Move the caller-session  rename to after the cron write commits, so a
  failed create (over-cap/duplicate 409, write 500, generation rollback)
  never leaves the caller's pre-existing session permanently renamed with
  no owning task (nothing restores the prior display name).
- On SessionNotFoundError, consult SessionService.getSessionLocation so an
  archived session — removed from the live map by archiving — still gets
  the documented 409 session_archived instead of a bare 404.
- canonicalizeWorkspace re-throws non-ENOENT filesystem errors (EACCES/EIO/
  ELOOP/ESTALE); surface those as a retryable 500
  scheduled_tasks_session_failed with a stderr log instead of a misleading
  400 session_workspace_mismatch.
- Parse sessionId with parseCallerSuppliedSessionId, the parser every other
  caller-supplied-session-id surface uses: UUID grammar, case-normalized,
  length-bounded (no unbounded echo in error bodies/stderr), and
  duplicate-binding equality per session rather than per spelling.

New tests: disk-backed archived fallback (runtime harness), ELOOP 500,
generic lookup-failure 500 with no side effects, over-cap rejection on the
reuse path, concurrent-create single-bind invariant (updateCronTasks
serializes writers; deleting the under-write-lock check flips the second
response to 201), null→mint, and padded/mixed-case normalization. Stub
session ids migrate to valid UUIDs to match the shared grammar.

* fix(serve): classify persisted-but-not-live sessions in task binding probe

The scheduled-task binding disk probe only special-cased 'archived';
'active' and 'conflict' locations fell through to a 404 that misreported
existing resumable sessions as nonexistent (routine after daemon restarts,
when only task-bound sessions are rehydrated). Answer 409 session_not_live
/ session_conflict for on-disk states and reserve 404 for genuinely absent
ids; add the findSessionIdIgnoringCase fallback for legacy uppercase-spelled
session files (mirrors session-id-admission). Also drop the dead
isArchived switch the bridge never populates, dedup the repeated rename /
lookup-failure bodies behind shared closures, align the invalid_session_id
message with the sibling caller-id surfaces, and pin the new behavior plus
the post-commit rename-failure invariant with tests.

* fix(serve): gate scheduled-task delete teardown on session ownership

Persist whether a task's bound session was minted by the task
(sessionOwnedByTask on DurableCronTask) and only close it on DELETE
when the task owns it — a caller-provided session pre-existed the task
and must survive its deletion. Tasks written before the marker keep
today's teardown (their bound sessions were always task-minted), and
the keepalive stamps ownership when it binds a freshly minted session.

Also stop mapping real filesystem failures in the persisted-session
probe to 404 session_not_found: the probe helpers rethrow non-ENOENT
errors (EACCES/EIO/ESTALE), which now surface as a retryable 500
scheduled_tasks_session_failed with a stderr log, matching the sibling
canonicalizeWorkspace catch in the same block.

Keepalive naming now uses the same payload as the route (task.name ??
task.prompt), so the post-restart sweep no longer clobbers the route's
 name on bound sessions (matters now that caller-provided sessions
are named by the route too).

* fix(serve): close session-binding races in scheduled-task create/reuse

R4-1: re-validate a caller-provided session under the cron write lock;
archive/delete tears the session out of the live map before its cron
hook runs, so a session that left the map between validation and commit
is now rejected with 409 session_not_live instead of binding a
201-returned task to an archived/deleted session.

R4-2: the in-lock duplicate-binding check now covers just-minted
sessions too (boundSessionId, not only providedSessionId) and runs
before the cap check; the alreadyBound branch no longer rolls the
session back, since a committed owner task means a concurrent
reuse-create won the race and owns the session.

R4-3 (narrowed, not closed): DELETE re-reads the cron file right before
closeSession and skips teardown when a surviving task references the
session; the residual re-read-to-close window needs session-scoped
serialization shared with the bind path (follow-up).

R4-4: keepalive bind writes also bail when any committed task already
references the just-minted session, mirroring the route's in-lock
check.

R4-5/R4-6: add the missing discriminating tests (mint-site naming,
sessionOwnedByTask validation); both mutation-verified.

* fix(serve): keepalive must not tear down a session a committed task owns

The round-14 review caught a regression in the duplicate-reference bail:
it routed the "a committed task already references the just-minted
session" case into the orphan rollback. In production wiring
cleanupSession is deleteDaemonSessionIfOrphan, whose requireZeroAttaches
passes for a just-minted session, and whose persisted removal cascades
removeTasksForSessions — so the rollback killed the race-winning task's
live session AND deleted its committed task from the cron file.

The two no-write bail reasons are now distinguishable: the
committed-reference check runs first and, when it fires, keepalive logs
and continues without cleanup — the session is left to its owner
(mirroring the route's symmetric alreadyBound branch, which performs no
rollback for exactly this reason) and this task stays unbound on disk
for the next tick to retry with a fresh session. The original bail
(task no longer bindable) still rolls the orphan back, unchanged.

Also pin three load-bearing behaviors that had no coverage: the
duplicate-check-before-cap-check ordering at the cap boundary
(session_already_bound, never max_tasks_reached with rollback), the
DELETE pre-close re-read failure fallback (still closes the owned
session), and the under-lock re-validation generic-error branch (500,
never coerced to session_not_live). All three mutation-verified.

* fix(serve): serialize scheduled-task session teardown with reuse-create binding (#9415)

* fix(serve): extend scheduled-task session teardown serialization to rollback and keepalive sites (#9415 R6)

* fix(scheduled-tasks): narrow existing session reuse

* fix(scheduled-tasks): restore conversation-bound tasks

* fix(scheduled-tasks): restore conversation runtime tasks

* fix(scheduled-tasks): honor the session-management gate on the primary surface

* test(scheduled-tasks): isolate the Conversations runtime ownership record

The boot-restore test passed no liveDiscoveryStableBaseDir, so
runQwenServe resolved it to ~/.qwen and built the Conversations-runtime
ownership on the machine-global record. A concurrent live owner under
the same HOME (another vitest worker, a shared-runner CI job, a
developer's qwen serve) failed the boot with
'The Conversations runtime is owned by another daemon.' Point the test
at a temp stable base, matching the four daemon boots in
run-qwen-serve-live.test.ts.

* test(scheduled-tasks): cover the ambiguous session-owner rejection path

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-24 02:23:45 +00:00
易良
2855149d47
fix(core): detect long verbatim repetition loops in content and reasoning streams (#9668)
* fix(core): detect long verbatim repetition loops in content and reasoning streams

The chunk-hash content loop rule only treats repeated 50-char chunks as a
loop when their occurrences cluster within 1.5 chunk lengths (75 chars), so
a verbatim-repeated unit longer than that (the ~300-char analysis block
chanted in issue #1775) never fires. Add a long-period rule: five equally
spaced occurrences of an identical chunk mark a candidate period, and the
spanned region is verified to be exactly periodic with that stride before
halting. Raise the content history window so long units stay observable.

Also route thought text into the content-repetition detectors when the
structured thought check does not fire: OpenAI-compatible providers stream
reasoning as thought parts that getResponseText filters out of Content
events, so chants in the thinking stage never reached the chunk-hash rules.

* fix(core): isolate reasoning deltas from the content channel's markdown state

Route thought-sourced text through an append-and-analyze-only entry point
instead of checkContentLoop. Reasoning text is raw chain-of-thought, never
rendered markdown: an unbalanced code fence in a thought used to flip the
shared inCodeBlock parity — which nothing clears mid-turn — silently
disabling visible-content chant detection for the rest of the turn, and
list/heading-shaped thought deltas reset the shared history, erasing
already-accumulated content evidence when a provider interleaves thought
and content parts.

* fix(core): grow the periodic-rule verified region with the repetition count

The long-period rule only inspected the last five occurrences, pinning the
verified region at 4 x stride + 50 chars: units of ~76-237 chars fell in a
gap between the clustered rule's 75-char bound and the 1000-char region
floor at any repetition count, and units of ~1 KB or more could never fit
five occurrences into the 4000-char history window at all. Extend the
candidate run backwards over the longest equally-spaced suffix of
occurrences so the verified region grows with the repetition count, and
once the history saturates accept a shorter run (>= 3 occurrences) when the
entire retained region is verified periodic back to the history start, so
earlier occurrences truncated out of the window cannot hide a chant. Also
correct the constants' comments describing the rule's domains.

* test(core): cover post-truncation chant detection after a long varied turn

Add the realistic #1775 shape that had no positive coverage: a long varied
turn filling the history window, then a ~700-char chant streamed as
misaligned deltas. Asserts detection at exactly the fifth in-window
occurrence, pinning MAX_HISTORY_LENGTH, truncateAndUpdate's index
adjustment, and the long-unit case together — a shrunken window would fire
early via the truncated-run path once the filler flushes, and a broken
index adjustment would never fire.

* fix(cli): widen chanting halt label to cover reasoning-stream repetitions

Reasoning-stream chants fire CHANTING_IDENTICAL_SENTENCES via
checkReasoningContentLoop, but getResponseText filters reasoning out of
visible output, so the headless label 'repeated the same sentence in its
output' sends users looking for a repetition that is never rendered.
Widen the label to 'output or reasoning' and add a headless-path
regression test asserting the wording.

* refactor(core): share the append/truncate/analyze tail across loop channels

checkReasoningContentLoop duplicated the streamContentHistory append,
truncateAndUpdate, analyzeContentChunksForLoop tail of checkContentLoop,
leaving the history contract in two copies that a future fix could let
drift. Extract the tail into appendToContentHistoryAndAnalyze and call
it from both entry points.

* perf(core): compare periodic regions in place instead of slicing history

isRegionPeriodicWithStride sliced up to ~4 KB of history per invocation.
Near-periodic chants fail verification repeatedly while their occurrence
runs persist, so once a run reaches length 5 the check fires on up to
every streamed character -- a probe measured ~136 MB of transient copies
over one 49k-char stream. Index the existing string directly instead;
comparison semantics are unchanged.

* fix(core): reset stream-content loop state on retry replays and model fallback

A replay (non-continuation) retry re-streams the failed attempt's
content and reasoning through the chunk detectors — the #7832
transport-replay gate admits thought-only cuts, and with deterministic
decoding the re-stream is verbatim. The Retry case in
addAndCheckHeuristicLoops cleared only the tool-call counters, so the
accumulated identical copies could fire CHANTING_IDENTICAL_SENTENCES
mid-way through an otherwise healthy attempt. Continuation retries
(isContinuation) keep the delivered text and append new output, so
their state stays. ModelFallback had no case at all: the fallback model
restarts from scratch, so mirror the replay resets for it. A genuine
chant simply re-accumulates after the restart.

* perf(core): defer content-history truncation with a hysteresis slack

Once streamContentHistory saturates, truncateAndUpdate walked the whole
contentStats map on every streamed event — Θ(window) entries in steady
state, since the stride-1 sliding window hashes every position
(~385 µs/event at window 4000 vs ~12 µs pre-saturation). With
high-frequency small reasoning deltas now routed through the path,
healthy long-thinking turns paid thousands of events of synchronous CPU.

Trim only when the length exceeds MAX_HISTORY_LENGTH by a
TRUNCATION_SLACK margin (1000 chars), slicing back to exactly
MAX_HISTORY_LENGTH, so the index-rebase walk is amortized over appended
chars. The change is behavior-neutral: the detection rules now always
operate on the logical window of the last MAX_HISTORY_LENGTH chars —
occurrences the window has passed are dropped at lookup (the exact set a
per-event trim would have removed) and the periodic rule's escape valve
verifies from the window start, i.e. exactly the content a fully-trimmed
history retains. Tests pin pre-change fire offsets across saturation and
multiple trims, plus the deferred-trim mechanics.

* feat(core): log a chanting-region excerpt on loop halt for debug

A reasoning-channel halt exits headless runs with empty stdout and only
the loop-type label on stderr; neither the LoopDetected event
(loop_type + prompt_id only), telemetry, nor any log carried an excerpt
of what repeated, leaving no way to tell a true repetition from a
detector misfire without instrumenting a repro.

Capture one period of the matched region (the span between the last two
occurrences, capped at 80 chars) when the chanting detector fires and
emit it through the config debug logger at the firing site. The
LoopDetected event contract is deliberately unchanged.

* fix(core): preserve subagent continuation retries

* test(core): cover plain subagent retry forwarding

* fix(core): omit plain retry continuation flag
2026-08-24 02:21:54 +00:00
nas
b2edb80a57
fix(cli): probe microphone permission on recording start, not voice warmup (#8912)
* fix(cli): probe microphone permission on recording start, not voice warmup

Voice warmup called recorder.microphoneStatus() as soon as the input
prompt mounted with voice dictation configured. On macOS an undetermined
TCC status maps to 'prompt', so every startup appended a "Voice dictation
needs microphone access" notice to the chat history, including for users
who never record.

warmupVoice now only preloads the recorder backend. The permission probe
and its 'denied'/'prompt' notices move to a checkMicrophonePermission
callback that useVoiceInput invokes from startRecording, so the notice
reaches only users who are actually trying to dictate.

The dedup ref moves up to Composer and reaches InputPrompt as an optional
prop, matching clipboardUnavailableShownRef. A per-instance ref reset on
every InputPrompt remount, which is what produced the duplicate notice.

Fixes #8877

* fix(cli): hold voice mic-permission dedup in AppContainer, not Composer

Dialogs (tool approvals, auth, settings) swap Composer out of the layout,
so a ref held in Composer reset on every dialog round trip and the notice
could repeat on the next recording. The ref now lives in AppContainer,
which owns dialogsVisible and never unmounts, and reaches InputPrompt
through uiState like mainControlsRef.

Also from review: delegate setupRecorder to setupRecorderWith in the
InputPrompt tests, cover the prompt->denied status transition (re-warns
as an error), and assert Composer forwards the session ref with stable
identity across input-active toggles.
2026-08-24 02:15:04 +00:00
Dragon
78eadd4bf1
feat(core): declare create_sub_session only under qwen serve (#9425)
* feat(core): declare create_sub_session only under qwen serve

create_sub_session needs the daemon bridge, which only exists under `qwen serve`, yet it was declared in every session. Interactive TUI and headless runs therefore carried a tool that can never succeed, polluting the model's action space and ToolSearch results. The tool is now registered by the ACP session at the same point it wires the sub-session spawner, so it exists exactly where it can work and nowhere else.

* fix(core): keep create_sub_session on registries built with a wired spawner

Dropping the unconditional registration also dropped the tool from every
registry rebuilt after the daemon session starts: sub-agent and override
registries are built through createToolRegistry with forSubAgent, and
copyDiscoveredToolsFrom carries discovered tools only, never built-ins.
Daemon sub-agents therefore lost the capability silently.

Restore the lazy registration but gate it on a sub-session spawner being
wired onto the Config, so interactive, headless and SDK runs still do not
advertise a tool that cannot work there, while daemon sub-agent and
override configs pick it up through prototype delegation. Going back
through the lazy path also restores the PermissionManager.isToolEnabled
gate for these registries.

Harden the negative test to assert on both registration entry points; a
regression that re-adds the tool eagerly never touches registerFactory,
so the previous assertion would have stayed green. Add a positive test
covering a subagent registry rebuilt after the spawner is wired.

* docs(core): align setSubSessionSpawner doc with the new gate

The setter's JSDoc still described the pre-PR behaviour — that leaving the
spawner unset makes the tool report itself as daemon-only. With the
spawner-gated registration the tool is never registered in interactive TUI
or headless, so nothing reports anything there, and the comment contradicted
the three sibling doc sites this PR already updated.

* fix(cli): permission-gate the daemon create_sub_session registration

The eager registration in the Session constructor called
ToolRegistry.registerTool() directly, which honors only `tools.disabled` —
so a daemon whose operator restricts `tools.core` or denies the whole tool
still advertised create_sub_session and failed every call with
EXECUTION_DENIED, exactly the "declared but unusable" pollution this change
set out to remove.

Registration now lives in an awaited helper that applies the same
PermissionManager.isToolEnabled() check the core-side gate in
createToolRegistry applies, and the daemon calls it once per session it
creates, after the Session has wired the spawner and before the session is
published. Also drops the unused CreateSubSessionParams public export.

* fix(cli): declare create_sub_session only on daemon-backed sessions, revealed to the model

Address the R4 review findings:

- Wire the sub-session spawner only when the daemon's QWEN_CODE_SERVE=1
  stamp is present. A standalone --acp session's peer is the editor,
  which answers the bridge's qwen/control/* ext methods with JSON-RPC
  -32601, so the tool was declared there but could never run. Gate
  registerCreateSubSessionTool on the spawner being wired so the tool
  exists exactly where it can execute.
- Reveal the deferred tool and refresh the declaration snapshot after
  registering: the registration lands after startChat() froze the chat's
  declarations, so without the reveal the model was never offered the
  tool for the session's first lifetime.
- Pin that newSession awaits the registration before the session is
  served, so the first prompt's declarations always include the tool.

* fix(core): pin the create_sub_session reveal across /clear resets

The reveal applied at daemon-session creation was permanently lost by the
first /clear whenever the deferred-tool startup preload did not fit its
all-or-nothing schema budget (or was disabled by a <= 0 / non-finite
operator threshold): resetChat() clears the revealed set, the preload
restores nothing, and registerCreateSubSessionTool never re-runs — the
tool silently dropped out of the declaration list for the rest of the
session.

Add ToolRegistry.pinDeferredToolReveal(): pinned reveals are session-setup
state (not ToolSearch discovery) and are re-applied by
clearRevealedDeferredTools() while the tool stays registered and deferred,
so the fresh session's startChat -> setTools() re-declares it. Pin
create_sub_session at registration.

* test(cli): pin create_sub_session registration on the permission-manager-enabled path

* docs(core): correct DAEMON_ONLY_MESSAGE reachability in create-sub-session header

Per wenshao's runtime verification (N2): the guard is reachable only for
a daemon session whose spawner was cleared mid-flight; in non-daemon
sessions the tool is absent from the registry, so a stale direct call
hits the registry-miss error before execute() is reached.
2026-08-24 02:07:23 +00:00
tlysanhuo
a369b4fac6
fix(cli): prevent input border overflow on resize (#8991)
* fix(cli): prevent input border overflow

* fix(cli): harden border width invariant
2026-08-24 02:07:12 +00:00
zhou2024NAU
9b27184903
fix(cli): normalize win32 drive-letter casing in MCP approval keys (#9779)
* fix(cli): normalize win32 drive-letter casing in MCP approval keys

Windows paths are case-insensitive, but the two entry points that produce a project root disagree on casing: the CLI stores process.cwd() as typed (D:\project) while IDE integrations pass VS Code's workspaceFolders[0].uri.fsPath with a lowercased drive letter (d:\project). normalizeProjectRoot() only resolved the path, so an approval recorded by the CLI was invisible to the IDE and the server showed as configured-but-pending.

Fold case on win32 following the existing getProjectHash()/sanitizeCwd() convention, and fold stored keys at load time so decisions written by older builds are not orphaned; duplicate-cased keys merge into one entry and are rewritten normalized on the next save.

Fixes #9775

* fix(cli): address review feedback on win32 MCP approval key folding

Fold only win32 absolute paths (drive-letter/UNC) at load time so foreign POSIX keys synced from a Linux machine round-trip verbatim (R1-4). When duplicate-cased keys collide, prefer the rejection over an approval: records carry no timestamps and file order does not track recency, so a stale approval can never re-enable a server the user rejected (R1-2). Reuse isApprovalRecordMap in setState instead of the inline duplicate predicate (R1-3). Make approve.test.ts persistedStatus() fold the lookup on win32 to match the stored key shape, so the suite passes on Windows runners whose temp path contains uppercase letters (R1-1). Add win32 regression tests: non-drive component casing, POSIX-key preservation, rejection-wins merge (R1-5).

* fix(cli): address round-2 review feedback on MCP approval key folding

Use a null-prototype merge target so a server named __proto__ keeps its decision when duplicate-cased project keys merge on win32 (R2-1). Skip non-record values during merge so a corrupt null record no longer throws and poisons the whole approvals file (R2-2). Pin the rejection-wins merge invariant across both key orders (R2-3) and add a legacy UNC-key fold case (R2-4).

* test(cli): cover null-record guard when merging case-collided win32 keys

The non-record-value guard in mergeApprovalRecords (added for R2-2) was only reachable on win32 via normalizeStoredProjectKeys and had no test exercising the merge path, so deleting it shipped green on every platform. Add a win32-gated case: two case-variant keys for one project where the later-iterated key holds a null record value. Assert the load succeeds with no errors and the valid decision under the other casing survives. Addresses the round-3 review suggestion.

* chore: re-trigger automatic review

---------

Co-authored-by: zhou2024NAU <zhou2024NAU@users.noreply.github.com>
2026-08-24 01:33:03 +00:00
callmeYe
eea98f3b04
refactor(cli): extract ACP skill management (#8865)
* refactor(cli): extract ACP skill management

* test(cli): cover ACP skill safety guards

* fix(cli): harden ACP skill mutation guards

* fix(cli): handle ACP skill frontmatter variants

* test(cli): deduplicate ACP skill fixtures

* fix(cli): handle multiline Skill enablement fields

* fix(cli): recognize escaped Skill enablement keys

* refactor(cli): restore ACP skill extraction scope

Restore the three post-review files to the initial extraction commit. The removed changes addressed pre-existing Skill behavior and test coverage rather than regressions caused by the module split. Latest origin/main changes only unrelated ACP agent sections, so no extracted Skill logic needs to be carried forward.

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-24 01:18:22 +00:00
qwen-code-dev-bot
ed5c56d840
fix(core): clear tool display list before awaiting completion callback (#9602)
* fix(core): clear tool display list before awaiting completion callback

The TUI completion callback commits the finalized tool_group to history
and then awaits the tool-result continuation, which since #9121 spans
the entire next model turn. The display-list clear was chained after
that callback in the finally block, so the completed group stayed in
the live pending list - pinned at the bottom of the virtualized list -
until the next tool call arrived or the loop ended (#9420, regression
in v0.21.13; v0.21.12's fire-and-forget submission cleared same-frame).

Notify observers that the display list is empty immediately before
invoking the completion callback (no await in between, so the clear and
the history commit land in the same React render); the finally-block
notify remains as the error-path fallback. Adds a regression test that
fails on main.

* test(core): strengthen finally-notify assertion in display-clear regression test (#9602)

* fix(cli): hold in-flight flags across the tool completion callback window (#9602)

---------

Co-authored-by: qwen-code-dev-bot <269191875+qwen-code-dev-bot@users.noreply.github.com>
2026-08-24 01:17:38 +00:00
Shaojin Wen
3a1f86d805
feat(review): give verifiers a do-not-refute list and a constructible rejection bar (#9799)
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
npm cache producer / Save npm cache (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
Security Checks / Dependency CVE audit (push) Waiting to run
Security Checks / Secret scan (TruffleHog) (push) Waiting to run
* feat(review): give verifiers a do-not-refute list and a constructible rejection bar

Step 4's verifier brief already floors uncertain Criticals at low
confidence instead of rejection, but it never names the states in
which "too speculative / depends on runtime state" is not a valid
rejection. The finder side carries the recall rule (do not silently
drop a candidate); the verifier side lacked its counterpart, so
real-but-uncertain findings could die in Step 4 on a plausibility
vote instead of surfacing under "Needs Human Review".

Close the same leak on the verifier side:

- Rejection is now defined as direct counter-evidence constructible
  from the code — one of four shapes: factually wrong (quote the
  misread line), provably impossible (type/constant/invariant,
  shown), already handled in this diff (cite the guard and show it
  covers the trigger), or pure style / an Exclusion Criterion. A
  rejection constructing none of them downgrades to confirmed (low
  confidence) instead of dropping.
- A third masquerading state joins "I could not verify it" and "its
  evidence is somewhere I did not look": "it is too speculative". A
  finding whose failure scenario names a realistic state the code
  does not exclude is PLAUSIBLE by default — concurrency races,
  nil/undefined on a rare-but-reachable path, falsy zeros treated as
  missing, off-by-one on a boundary the code does not exclude, retry
  storms and partial failures, patterns that lost an anchor.

SKILL.md's Step 4 summary and the user-facing code-review docs are
synced to the new semantics. The pinning test asserts every shape,
every ground, and the downgrade consequence — a mutation flipping the
consequence into "reject" survived the subject-only assertion, so the
consequence clause is pinned too.

Fixes #9789

* fix(review): sync the rejection-bar summaries with the brief's four grounds (#9799)

* fix(review): sync the plausible-by-default wording and re-head the probe option (#9799)

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-08-23 23:33:12 +00:00
Isaac Hernández
fd9c452dc8
fix(auth): let Vertex AI authenticate with Application Default Credentials (#9017)
* fix(auth): let Vertex AI authenticate with Application Default Credentials

Vertex AI auth required an API key, so an ADC or service account setup
could not start. Supplying a placeholder to satisfy the check made it
worse: an explicitly passed key switches the Google SDK to Vertex
Express mode, which clears the project and location and rejects the
request with "API keys are not supported by this API".

Treat a configured GOOGLE_CLOUD_PROJECT as sufficient credentials for
the vertex-ai auth type, in both the CLI pre-flight check and the core
model config validation, and leave the API key absent so the SDK
resolves ADC itself. The missing-credentials errors now mention the
keyless path instead of pointing only at envKey.

Fixes #9016

* fix(auth): select Vertex mode explicitly and keep declared key vars authoritative

Review follow-ups on the Vertex ADC change.

Vertex mode no longer depends on the GOOGLE_GENAI_USE_VERTEXAI side effect.
Only the CLI pre-flight check writes that variable, and the startup call to it
sits under the sandbox branch, so a plain interactive or ACP session built a
client pointed at the Gemini API endpoint instead of Vertex. The flag is now
derived from the auth type at construction, and left untouched for the other
auth types so the SDK keeps its own environment fallback there.

An entry that declares its own key variable no longer falls through to ADC when
that variable is unset. It keeps failing on the declared variable, so a secret
that failed to inject cannot silently authenticate as a different principal.
The keyless hint is suppressed for those entries as well, since it would be
advice that cannot work.

The ACP pre-flight cell reports an indeterminate state for a keyless Vertex
setup rather than a confirmed token: a configured project is routing
configuration, not evidence that a credential resolves. All three gates now
share one definition of a configured project, so whitespace is handled the same
way everywhere, and the CLI missing-key message carries the same keyless hint as
the core errors.

Docs corrected on two counts: the environment-only row now says a keyless setup
must select the auth type explicitly, since it is not inferred from the project
alone, and the provider note names every key source the resolver folds in.

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-23 19:27:16 +00:00
callmeYe
c2d63fbe58
fix(web-shell): show reasoning effort before session creation (#9599)
* fix(web-shell): show reasoning effort before session creation

* fix(web-shell): harden reasoning preview lifecycle

* chore(desktop): refresh frozen bun lockfile

* fix(web-shell): restore reasoning preview after session clear

* test(webui): pin session-clear model restoration on all reset paths (#9599)

Witness the four back-to-welcome reset handlers' models re-projection
(session_closed, stream auth failure, terminal stream error, heartbeat
clear) with mutation-visible assertions: each test attaches a session
whose live context displaces the provider models, then verifies the
workspace reasoning preview returns after the reset. Also pin the
providers-absent fallback in getConnectionAfterSessionClear so older
daemons keep the pre-clear model list.

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-23 19:11:19 +00:00
Nothing Chan
d52bb4d678
fix(config): allow prompt hooks in settings schema (#8779)
* fix(config): allow prompt hooks in settings schema (#8752)

* fix(cli): make prompt hook schema test type-safe

* test(cli): preserve hook type schema coverage

---------

Co-authored-by: nothing <nothing@U-DQY4PXFJ-0222.local>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-23 18:39:05 +00:00
samuelhsin
4ddbf227e8
feat(mcp): add MCP 2026 core and WebShell Apps host (#8992)
* feat(mcp): add 2026 protocol negotiation

* feat(mcp): render MCP Apps in WebShell

* fix(mcp): keep legacy tool discovery lenient

* fix(mcp): keep Apps HTML out of TUI and honor tool visibility

TUI and history compaction dumped mcp_app HTML as JSON, and discoverTools registered app-only tools for the model.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): stabilize AppBridge lifetime and close sandbox CSP gaps

Theme toggles and transcript reseeds were tearing down MCP Apps; the host CSP also allowed any loopback port and form posts bypassed connect-src.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): list under-declared modern MCP capabilities over the wire

v2 typed helpers return [] without a request when a capability is omitted.
Use them only when the server declared the capability, and keep Apps
unmounted in collapsed tool rows.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): keep Apps sandbox reachable and list past 64 pages

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): reject empty compacted html and keep MCP Apps expanded in multi-tool groups

Fixes R3-1 and R3-2 review comments:

R3-1: getMcpAppDisplay now rejects empty html strings (from compaction)
so session replay shows fallbackText instead of mounting an empty iframe.

R3-2: ToolGroup now checks for MCP apps across all tools (not just
singleTool), auto-expands when any tool has an MCP app, and keeps
MCP app rows expanded (summaryOnly=false, forceExpanded=true) even
when adjacent tool calls are merged into the group.

* feat(web-shell): fold thinking into the compact-mode tool summary (#9148)

Compact mode used to drop thinking messages entirely, so a running turn
gave no indication of the thinking step. Keep the thoughts and aggregate
them with the adjacent tools into one summary: a streaming thought reads
"Thinking…" with the running shimmer, and a completed thought settles into
a click-to-expand row in its original interleaved position. The translate
action is preserved on both the thinking block and the folded thought
rows, and the merged group gets a synthetic id so its expanded state never
leaks into non-compact mode.

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>

* fix(mcp): address app discovery and sandbox regressions

* fix(web-shell): keep MCP apps expanded in compact summaries

* Revert "feat(web-shell): fold thinking into the compact-mode tool summary (#9148)"

This reverts commit ab2eebc5d36f17a51ce94e423db5745dcbb273fe.

* fix(web-shell): render compacted MCP App fallback and teardown before unload

Compacted history keeps type:mcp_app with empty html; show fallbackText instead of a blank sandbox, and wait for ui/resource-teardown before unloading the iframe.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): bound the discover probe and raise the daemon bundle cap

Silent legacy stdio servers inherited the 10-minute request timeout for server/discover. Cap the probe at 5s so fallback fits the discovery window, and raise the browser bundle budget after the main merge overflowed CI by 47 bytes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): skip version-negotiation probe on remote transports

SDK v2 rejects HTTP server/discover timeouts without falling back to initialize, and the 5s probe consumed the entire remote discovery window.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web-shell): keep MCP App iframe src across deferred teardown

Deferred unload() was clearing src on the live iframe after a remount, so the new AppBridge never saw sandbox-proxy-ready.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): honor listing-level MCP App CSP and permissions

registerAppResource puts ui.csp/permissions on resources/list, and resources/read does not merge that metadata into content entries.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): reuse session client for list and emit app fallback text

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): keep mcp list and IPv6 sandbox CSP valid

Give qwen mcp list leftover handshake budget after the 5s discover probe, and stop emitting invalid [::1] CSP origins.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): keep modern list and short discovery budgets working

Drop the era-illegal ping after mcp list connect, shrink the stdio discover probe to the discovery window, and document that remotes stay on legacy initialize until the SDK can fall back.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): keep the 2026 slice free of review-only extras

Drop the global tools/list page cap, generated companion notices, and the review screenshot so this PR stays on stdio 2026 plus the WebShell Apps host.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): restore generated companion notices after the SDK v2 bump

CI regenerates NOTICES.txt from the lockfile; the file has to ship with the new MCP client dependencies.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): isolate the Apps proxy from WebShell storage

Drop allow-same-origin on the outer sandbox iframe so a default localhost daemon cannot read the WebShell session token.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): harden fallback and app sandbox

* fix(core): preserve large and app-only MCP catalogs

* fix(mcp): preserve legacy negotiation compatibility

* fix(mcp): default stdio negotiation to legacy

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: ytahdn <1294726970@qq.com>
Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
Co-authored-by: YungSen Hsin <yungsenhsin@U-G0HXNQM1-2052.local>
2026-08-23 18:34:30 +00:00
易良
56db17bd4c
refactor(cli): enforce utils leaf-layer dependency direction (#9146) (#9737)
* refactor(cli): enforce utils leaf-layer dependency direction (#9146)

Move domain-coupled modules out of packages/cli/src/utils into the
directories that own them: config/ (dialogScopeUtils, settingsUtils),
i18n/ (languageUtils), ui/ (handleAutoUpdate, standalone-update,
systemInfo, systemInfoFields, update-relaunch, commands, doctorChecks),
nonInteractive/ (nonInteractiveHelpers, chat-recording-failure,
tool-result-boundary-diagnostics, permission-suggestions), serve/
(sandbox), services/housekeeping/ (scheduler, non-interactive-scheduler),
and commands/review/ (findings).

Extract the generic normalizePartList helper into
utils/normalize-part-list.ts so utils consumers keep importing downward,
and move the MergeStrategy enum into utils/deepMerge.ts (its owner).

Add an eslint architecture rule (no-utils-upward-import) that forbids
value imports from utils/ back up into a domain directory. Type-only
imports stay exempt: they are erased at compile time and cannot create a
runtime cycle (Settings in modelConfigUtils, CommandContext in
sessionPaths).

No behavior change: typecheck, build, and the affected unit tests pass.

* fix: use Qwen Team 2026 license header on new files (#9146)

* chore: refresh stale utils/ path references after leaf-layer move (#9146)

* docs: reconcile no-utils-upward-import header with the allowed type-only set (#9146)

* fix(cli): allowlist sandbox process.env accesses after leaf-layer move (#9146)

* chore(ci): re-record qwen-autofix.yml size baseline after #9677 (#9146)

#9677 recorded qwen-autofix.yml at 392111 bytes while the file it
committed was already 397656, so every PR that merged main after it
tripped the growth ratchet. Re-record the actual size; the file itself
is unchanged by this PR.

* fix(review): drop the stale utils/findings.ts digest root after the leaf-layer move (#9146)

The #9146 move returned findings.ts to commands/review/, but the digest
root lists merged from main still pinned it under utils/, where the file
no longer exists — the absent root darkened every review's staleness
check and failed review-source-digest.test.ts. Drop the stale file-shaped
root from both digest copies and their pins; the commands/review/
directory root covers the validator at its new home, and the two utils
helpers keep their file-shaped roots.

* fix(review): colocate seatbelt profiles with the sandbox module (#9146)

* fix(review): exempt inline type-only specifiers from the utils upward-import rule (#9146)

* fix(review): report upward inline type-specifier imports under verbatimModuleSyntax (#9146)

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

* test(review): pin mixed-specifier and zero-specifier upward imports in the utils rule (#9146)

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

* test(review): anchor the nested-checkout utils rule fixture on the last marker (#9146)

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

* test(review): pin that the utils/findings.ts digest root stays removed (#9146)

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

* fix(review): reword stale-bundle SCOPE header to the post-move helper shape (#9146)

* test(review): drop the pre-move utils/findings.ts from the skill-parity fixture (#9146)

* test(serve): derive the seatbelt colocation tripwire from BUILTIN_SEATBELT_PROFILES (#9146)

* fix(architecture): fail closed on computed dynamic imports in the utils leaf rule (#9146)

* fix(cli): point settings.test.ts at the post-move settingsUtils path (#9146)

main updated settings.test.ts after this branch moved settingsUtils.ts
from utils/ into config/, and the merge kept main's old import
specifier, which vite fails to resolve. Repoint it at ./settingsUtils.js;
every other consumer already uses the new path.

* fix(cli): close utils boundary review gaps

* test(cli): cover utils boundary allow paths

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-23 14:41:49 +00:00
Shaojin Wen
f877fb3525
feat(review): add the persistently-critical convergence advisory (land-with-residual-risk) (#9526)
* feat(review): add the persistently-critical convergence advisory

The severity floor converges a healthy loop — Suggestions stop posting and
the volume falls to the Criticals, then to zero as those get fixed. But a
loop whose Criticals never clear — the security-sensitive PR under
adversarial review — posts Criticals every round forever: the floor engages,
the Suggestions stop, and the volume flatlines at the Critical count instead
of falling. Nothing before this said so.

This adds the shape detector and its ONE recommendation:

- lib/convergence.ts — `convergenceAssessment` computes one fact from the
  carried telemetry (Criticals stood in the previous round's work-list AND
  stand again this round, with the two-round posting window present and not
  shrinking) and, when it fires, returns the `land-with-residual-risk`
  recommendation. Pure data, never authority: no threshold, no blocking, no
  merge/close — every input degrades OPEN, so absence is fail-safe, never a
  suppressed finding.
- compose-review wires it: `prevLedgerFacts` now recovers the previous
  work-list's Critical presence beside the round and volume; the assessment
  surfaces on three surfaces — a structured `convergence` field on the
  composed JSON, a rank-1 non-capping body disclosure, and a terminal
  CONVERGENCE line — each advisory-only and self-disclaiming, with a blank
  residual-risk inventory scaffold (attack surface · attacker-dependency ·
  blast radius) for the maintainer's risk-acceptance decision.

The exit the floor cannot provide: when the loop is provably stuck on
Criticals, the tool names the maintainer's decision (merge, carrying the
residual risk) instead of opening another round. Advisory only — it never
blocks this review.

Closes the convergence-exit gap in #9278; evidence and design in #9410.

* fix(review): surface the convergence advisory on every reachable event, gated on floor engagement (#9526)

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

* fix(review): give the trimmed convergence advisory its own disclosure rank (#9526)

The advisory shared trim rank 1 with the deferral display, but every
rank-1 disclosure surface names "the deferred-findings list" — a fired
zero-deferral round whose body overflowed posted a trim notice asserting
a deferral list that never existed while the dropped advisory went
unnamed. The advisory now holds its own rank (and RANK_NAMES entry),
yielding after the deferral display and before the not-reviewed
disclosures. Adds the overflow fixture that pins the yield and the
relocated-arm firing fixture that pins the third thisCriticals term,
and corrects the prevLedgerFacts threat docstring: under `auto` the
floor-engagement conjunct is forgeable via the carried round, so the
only unforgeable conjunct is this round's own standing Critical.

* fix(review): count the script-lint gate's Criticals in the convergence signal (#9526)

The persistently-critical signal read `thisCriticals` before the gate
pushed its Criticals into `bodyCriticals`, and the ledger work-list
feeding the next round's persistence half omitted them too. A loop
whose standing blocker is the deterministic [lint] gate — the exact
shape the signal exists to name — held the whole conjunction
semantically while the advisory stayed silent: the count was taken
before the array was complete, and the gate-only round recorded no
sev 'C' for its successor to recover.

The assessment now runs after the relocated and gate pushes and reads
the completed array with the same semantics as the verdict's own `c`
(the explicit relocated term drops with the push that already carries
it), and the gate's Criticals join the marker work-list. Adds the
handler fixture arming the gate end to end — advisory fires, marker
records sev 'C' — and pins both branches of the trim notice's
copy-location conditional, which had no oracle on either side.

* fix(review): close the round-5 oracle gaps on the residual-risk advisory (#9526)

Round 5 reviewed the merge that landed #9461 underneath this branch and
found four suppress paths the merge introduced with no end-to-end oracle,
plus one standing comment overclaim. Each finding was reproduced as a
surviving mutant before the fix and re-run after, so every test added here
is one that actually kills something.

R5-2 — `residualRisk` is carried into the durable artifact instead of being
omitted from it. The omission's stated reason ("the advisory rides the
persisted body") is false on exactly the rounds that need the record: rank 2
sheds before the not-reviewed disclosures, so a fired-but-trimmed round left
a maintainer reading `.qwen/reviews` a "did not fit" breadcrumb and none of
the facts behind the `land-with-residual-risk` call. Its sibling
`convergence` is allow-listed one paragraph up for that precise reason, and
the merge had put the two on opposite rules. Shape-checked like every other
field on this boundary rather than passed through.

R5-1 — the persistence conjunct had no silence fixture. Every firing
fixture carries sev `C` in the prev ledger, so replacing the derivation with
a bare `true` shipped the suite green while a round introducing its FIRST
Critical would fire `land-with-residual-risk`. Added a fixture whose
predecessor holds Suggestions only, all other conjuncts true.

R5-3 — the enforcement-vs-reporting floor reading had no oracle for the one
input where the two disagree: a genuinely ABSENT `severityFloor` at round
>= 6, which the reporting reading folds to `auto`. Every advisory fixture
passed `severityFloor: 'auto'` explicitly, so the swap shipped green and
would publish "The severity floor will not converge it" over a round whose
enforcement backstop moved nothing. Added a fixture with no `severityFloor`
key at all.

R5-4 — the two silence fixtures asserted only absences. `prevLedgerFacts`
swallows every recovery failure into round 0, so a predecessor that never
loaded produced the same silence and the arms they claim to pin were
vacuous. Both now assert the VOLUME line quoting the predecessor's volume
as a positive recovery sentinel.

R4-1 — the marker path's second `scriptLintGate` run is left in place: it
lives in a different function from the body composer's, threading the value
across would add a seventh positional parameter for plumbing, and the two
agree because the gate is pure in `planPath` over inputs immutable within
one synchronous compose. What was wrong was the comment claiming it was
"the same gate the body ran"; it now states the actual invariant and the
actual hazard — an edit that filters what the BODY pushes must change this
list too.

packages/cli: typecheck, ESLint and Prettier clean; src/commands/review
4301 pass / 1 skipped. Mutation matrix (compose-review + save-artifact,
475 tests): baseline green; `prevHadCritical: true`, the reporting-reading
swap, dropping `residualRisk` from the persisted verdict, and a vacuous
ledger recovery each turn the suite red.

* fix(review): act on the round-6 deferred list for the residual-risk advisory (#9526)

Round 6 posted no findings and deferred ten observations under the
convergence posture. Eight are addressed here; each was reproduced as a
surviving mutant first and re-run after, and the two that are not addressed
are recorded below with the reason rather than left silent.

Correctness:

- The volume window straddled a posture change. The round the floor engages
  on compares a Critical-only volume against a predecessor that was still
  posting Suggestions — a drop that is the posture, not the loop — and on a
  flat pair the advisory could publish "the severity floor will not converge
  it" after one round of the floor. `ConvergenceFacts` now carries
  `prevFloor` and a recorded `o` predecessor suppresses. Read the way the
  sibling diagnosis in the same module reads it: a floor that was never
  recorded is not a floor that DIFFERS, so pre-field markers evaluate
  exactly as before. Pinned in both directions — deleting the guard and
  tightening it to reject unrecorded floors each turn the suite red.

- `noteTrimmedRanks`' tail clause keyed on the advisory instead of on the
  disclosures. Over a combined rank-2-and-3 drop it read "another copy — the
  advisory also rides the composed JSON", telling the operator the trimmed
  set was backed up when the half that is not backed up was exactly the half
  the sentence exists to rescue; over a rank-0 drop it read "their only
  other copy" for a paragraph the composed result does carry. It now keys on
  rank 3. The artifact stays unnamed here — naming it sent the operator to a
  deferral list that does not exist, which the existing test caught.

- The terminal `RESIDUAL-RISK:` record spread one labelled line over seven,
  six of them unlabelled, because the advisory carries a markdown table for
  the body. Collapsed at the print site only: the pipes survive, so the
  inventory's three columns still reach the operator on the round where the
  body budget shed the formatted copy.

Accuracy of the record:

- The `PersistedVerdict` comment claimed `residualRisk` sheds "before
  anything else". It is rank 2; `convergence` is rank 0. What they share is
  that both CAN go.
- The bundled skill enumerated two of the four trim ranks and stated the
  no-durable-copy rule without its exception. Both assertions in
  `SKILL.test.ts` move with the prose.

New oracles (test-only):

- The advisory-only guarantee — the claim the whole feature rests on — was
  unpinned: a fired round now asserts the event stays where the findings put
  it and that `cappedBy` gains nothing.
- The floor-futility sentence was pinned only negatively; it now has a
  positive assertion in both languages.
- The zh advisory's scaffold columns and its Critical-count interpolation
  slot had no oracle. `FIRE` is deliberately asymmetric (2 Criticals, volume
  3/3) so a template reading the wrong slot shows.
- The rank-ordering guard could not tell rank 2 from rank 3; the combined-
  drop test closes it — the `trim: 3 -> 2` mutant now fails five tests.

Not done, deliberately:

- The validator does not re-assert `criticals >= 1` / `posted >= prevPosted`.
  Those are `convergenceAssessment`'s construction invariants, and a second
  statement of them at the save boundary is a rule free to drift from the
  first — with the artifact, the durable record, as what gets thrown away
  when it does. Identity is pinned instead (`shape`, `recommendation`) and
  the counts are shape-checked.
- The marker path's second `scriptLintGate` run stands (R4-1); the reasoning
  is on that thread and at the call site.

packages/cli: 4305 pass / 1 skipped. packages/core skills: 376 pass.
Typecheck, ESLint and Prettier clean on both workspaces.

* fix(review): measure the residual-risk window on fresh findings, not totals (#9526)

Round 7 posted one Critical and it is correct. The volume conjunct compared
posting TOTALS, and Step 6 re-posts every still-standing ledger Critical
under its original id — so the total only ever rises and a converging loop
reads as a stuck one. Reproduced through the real `composeReview` before
touching anything: round 6 posts 5 first-time Criticals; the author fixes 3;
round 7 re-posts the 2 that stand and drafts 4 new. Fresh 5 -> 4 is a loop
settling, the total went 5 -> 6, and the advisory fired
`land-with-residual-risk` over it.

The window now runs on the fresh pair the marker already carries —
`postedFresh` and `prev.fresh`, the same numbers the loop-settling
observation in the same module trends on, so the two features cannot
disagree about what a round produced. `prev.fresh` absent degrades open.

Applying only that change would have introduced a second false fire, so it
does not ship alone. The posting total was silently covering a case the
fresh window is blind to: a reviewer finding nothing new for two rounds
while the author clears blockers sits at fresh 0 against fresh 0, which
"not falling" reads as stuck. Probed on the pre-change code — backlog 5 -> 3
with zero fresh both rounds is silent today (3 < 5) and would have fired
under a fresh-only window. The assessment therefore also takes the standing
Critical count and vetoes on observed shrinkage. A veto rather than a
requirement, on positive evidence only: the work-list it counts is the one
the marker's byte budget may have shortened, and an undercount can only hide
shrinkage, never manufacture it — so an unknown predecessor abstains instead
of silencing a genuinely stuck loop.

Unlike the sibling diagnosis, this signal does NOT require `prev.fresh > 0`.
That module is about a loop generating work; this one is about work that
never clears, and Criticals standing round after round with nothing new is
the shape itself, not a quiet loop. The backlog veto is what separates it
from a backlog being worked down.

The reported numbers are renamed with what they now measure — `posted` /
`prevPosted` become `fresh` / `prevFresh` on `ConvergenceFacts`,
`ConvergenceAssessment` and the persisted artifact — and the advisory prose
follows in both languages. Feeding fresh counts into fields printed as "the
posting volume" would have swapped one false record for another.

Verified as five shapes through the real command, then pinned as tests: the
reported fresh-shrinking loop is silent; the clearing backlog is silent; a
pre-fresh marker is silent; and both firing shapes still fire — the same
Criticals re-posted at zero fresh, and new Criticals every round.

Mutation matrix (539 tests): reverting the window to totals, deleting the
backlog veto, and tightening the veto to suppress on an unknown predecessor
each turn the suite red.

packages/cli: 4310 pass / 1 skipped. Typecheck, ESLint and Prettier clean.

* fix(review): prove the predecessor's floor enforced, don't trust its stamp (#9526)

R8-1 is correct. The posture-change guard paired two different readings
across the window's ends: this round's engagement is the strict
`criticalFloorInEffect`, but the predecessor's `floor` stamp is written from
`criticalFloorKind`, the reporting fold — which folds an absent
`severityFloor` into `auto` and stamps `c` on any round >= 6 the enforcement
backstop never touched. Reproduced through the real code first:

    criticalFloorKind(undefined, false, 6)     = 'auto-resolved'  -> stamps 'c'
    criticalFloorInEffect(undefined, false, 6) = false            -> Suggestions post

so a predecessor that still posted Suggestions passed the guard, and the
advisory published "the severity floor will not converge it" one round after
enforcement actually started.

Neither fix direction the finding names is taken. Restamping the marker from
the enforcement reading would leave the sibling diagnosis comparing this
round's reporting stamp against a predecessor's enforcement stamp — the same
cross-reading defect moved into #9623's feature — and #9623 chose the
reporting reading deliberately, because its advice quotes the floor back to
the author. Special-casing a "newly named" floor needs the predecessor's raw
`severityFloor`, which no marker carries.

The evidence is already in the work-list instead. Enforcement moves drafted
Suggestions out of the posting set before the marker is built, so an engaged
round's list is Critical-only and an un-enforced one is not — measured
through the real composer across all four postures:

    floor=critical (engaged)              work list ["C"]      stamp c
    floor=auto, round 7 (engaged)         work list ["C"]      stamp c
    floor ABSENT, round 7 (folded c)      work list ["C","S"]  stamp c   <- the hole
    floor=suggestion (not engaged)        work list ["C","S"]  stamp o

`prevPostedSuggestion` is that fact, and it suppresses on the POSITIVE
observation so the two ways it can be wrong land on opposite sides: a
shortened list that shed its Suggestion reads as engaged (the truncation
caveat the backlog veto already carries), while a pathless Suggestion an
engaged round left inline reads as un-enforced and costs one round of
silence. Unknown abstains, like every other fact read off that list.

Mutation matrix: deleting the guard, tightening it so an unknown predecessor
suppresses, and pointing the wiring at the wrong severity each turn the suite
red — the first on both the unit arm and the end-to-end fixture built from
the finding's own witness.

packages/cli: 4432 pass / 1 skipped. Typecheck, ESLint and Prettier clean.

* fix(review): refuse a pure-foreign work-list as this account's history (#9526)

Correct, and reproduced through the real composer before changing anything.
Recovery adopts the highest-round marker whoever posted it. Where that marker
was NOT merged over this account's own findings, this account's entries are
in no work list at all — the state `openCriticals` already refuses to infer
across, one screen up in the same function. Every prev-round fact this signal
reads comes off that list, and it read it unconditionally:

    pure-foreign  {foreign:true, merged:false}   -> FIRES
    own list      {foreign:false}                -> FIRES
    merged        {foreign:true, merged:true}    -> FIRES

An own round-6 marker that was a clean LGTM (empty findings, fresh 0, floor
stamped `c`), a foreign same-round marker carrying Criticals and no
Suggestions winning recovery, and one Critical drafted this round were enough
to publish "Criticals stood in the previous round's work-list and stand again
this round — land-with-residual-risk" over this account's own LGTM.

All three list-derived facts are withheld on that state, not just
`prevHadCritical`: it alone silences the assessment today, but leaving the
other two reading a stranger's list is a hole waiting for the next edit to
re-open. `prevPostedSuggestion` in particular reads ABSENCE, and a stranger's
Critical-only list is exactly the shape that reads as "the floor enforced".

Merged foreign lists are deliberately NOT withheld: the union keeps this
account's own certified entries under their own ids, which is the part that
makes the list speak for this account again — the same distinction
`openCriticals` draws.

The test drives all three arms and asserts them as one table, so the fix is
pinned in both directions: a mutant disabling the gate fires on the stranger,
and a mutant widening it to any `foreign` marker silences the merged arm.
Both turn the suite red, as does un-gating `prevHadCritical` alone.

Not changed, and recorded rather than left implicit: a TRUNCATED work-list
still reads as this account's. Truncation shortens our own list, which is a
different thing from a stranger's, and the direction it errs in is already
documented on `prevPostedSuggestion` and the backlog veto. Requiring
completeness would silence the advisory on precisely the deep-work-list
rounds it exists for.

packages/cli: 4433 pass / 1 skipped. Typecheck, ESLint and Prettier clean.

* fix(review): stop a gate Critical compounding, and qualify a truncated reading (#9526)

Round 11's two Criticals. Both reproduced through the real composer before
anything was changed.

R11-2 — a standing gate Critical entered the posting set twice, and the pair
compounded. This is a regression from this branch's own commit d72287cc: once
the gate's `[lint]` Criticals are in the carried work-list, SKILL Step 6's
still-standing rule tells the model to re-post the entry under its original
id while compose re-derives the same Critical from the report. `buildLedger`
keys by claimed id and the regenerated copy claims none, so it minted a
second id beside the carried one:

    ROUND1  work-list [R1-1]                  blocker rendered once
    ROUND2  work-list [R1-1, R2-1]            rendered twice
    ROUND3  work-list [R1-1, R2-1, R3-1]      rendered three times
    FLIP (revert the gate spread): round-1 work-list [], rendered once

`withoutGateReposts` drops the re-post, keeping the GATE's copy rather than
the model's. That direction is load-bearing: `[lint]` is not in
`DETERMINISTIC_TAG_RE` (`[build]`/`[test]`/`[probe]` only), so the model's
copy counts toward `criticalsNeedingVerify` — a linter-proven blocker was
pulling the unverified-blocker cap on every re-post round, and the probe
shows that cap disappearing with the fix. For the same reason the dedup runs
BEFORE `modelBodyCriticals` is captured: dropping the re-post from the body
alone left provenance still counting it, so the first draft of this fix fixed
the rendering and kept the cap.

Matched on the gate line's LOCATOR (the `` `path`:line CODE `` it opens
with, backticks normalised), not the whole string: a re-post is model prose
that carries the entry forward without reproducing the message byte for
byte, and an exact-match rule stopped deduping the moment the wording
drifted. The carried id is stripped through the ledger's own
`LEDGER_ID_READBACK`. The body composer's gate call is now the only one on
that path, so this also removes one half of R4-1's double invocation.

R11-1 — the residual-risk facts are read off a work-list that may be
known-truncated, without the completeness gate `openCriticals` applies. The
completeness gate is NOT restored, and that is the same call as round 8: a
whole-list requirement would silence the advisory on exactly the
deep-work-list rounds it exists for, which are the rounds the byte budget
shortens. What was wrong is what the code SAID about it. The block comment
claimed "every input degrades open to no assessment"; two of these inputs do
not. "No Suggestion, so the floor was enforcing" and "the backlog is not
shrinking" are read off ABSENCE, and a shortened list can only lose entries,
so both lean toward firing.

`prevTruncated` now rides the facts and the assessment — deciding nothing —
and the paragraph discloses, in both languages, that those two readings came
off an incomplete list. The sibling diagnosis in the same module qualifies
its own recurrence reading on the same fact; this follows that precedent
rather than inventing one. The block comment states the exception instead of
the blanket claim.

Mutation matrix: never rendering the caveat, wiring `prevTruncated` to a
constant, disabling the gate dedup, reverting the dedup to exact-match, and
removing it from the marker work-list each turn the suite red — alongside the
carried set (window on totals, pure-foreign gate, enforcement-evidence
guard).

packages/cli: 4697 pass / 1 skipped. Typecheck, ESLint and Prettier clean.

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-23 12:40:24 +00:00
jinye
431a0bd9b0
fix(daemon): keep restored ask_user_question valid after load (#9763)
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
Security Checks / Dependency CVE audit (push) Waiting to run
Security Checks / Secret scan (TruffleHog) (push) Waiting to run
* fix(daemon): keep restored questions valid across load, send, and replay

Post-merge review of the restore path found illegal provider history, phantom rewind snapshots, dropped resume notices, and replay that finalized a question the load was about to re-hang.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(cli): pin ask_user_question restore suppress wiring in acpAgent

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

* fix(daemon): skip persistence for a whole restored batch that ends unattended

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

* test(core): pin restorable ask_user_question preservation on a real Config

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-23 05:03:31 +00:00
callmeYe
f1b1305a76
feat(models): support dual-role image generation models (#9650)
* feat(models): support dual-role image generation models

* fix(models): address dual-role image selector review

* test(cli): cover image model resolver rejection

* fix(models): preserve legacy vision image routes
2026-08-23 05:02:32 +00:00
Dragon
1e062a4d0f
perf(cli): raise VP scroll rendering to 60 FPS (#9681) 2026-08-23 04:50:28 +00:00
Shaojin Wen
509226260c
feat(review): back comment-status and presubmit for Aone Code targets (#9627)
* feat(review): back comment-status and presubmit for Aone Code targets

A second `--comment` round on an Aone MR re-posted every still-valid
finding as a new comment and never downgraded a self-MR review — both
flows were skipped for lack of a1 backing. Route Aone targets at the a1
reads (mr view / mr status / mr comment list / auth whoami) through the
same pure classification cores the GitHub path pins, so the report
schemas and the Step-7 downgrade semantics stay one contract:
parentNoteId threading, closed → resolved, outdated → stale (a
rewritten line stays re-postable), no commit anchors (code facts
degrade to unknown), and drift with no compare API fails safe. The
context-unavailable verdict cap stays until pr-context lands.

Closes #9613

* fix(review): harden Aone runners' pr_number guards and null gate payload

Address round-1 review findings on the Aone backing of comment-status
and presubmit:

- extractStatusChecks no longer throws a TypeError when a1 answers a
  bare null to `mr status`; the payload now reads as the designed
  unreadable gate state (undefined), capping the verdict like a
  still-running check instead of crashing presubmit with no report.
- comment-status and presubmit validate pr_number with fetch-pr's
  /^[1-9]\d*$/ grammar before Number() coercion, refusing '012'/'1e3'/
  '0x1f'/' 12'/'12.0' tokens that would query a different MR than the
  caller's label carries.
- Pin the two subject_type combinations no test covered (pathless
  comment WITH outdated:true; the live path+line shape) with
  mutation-probed assertions.
- Align the --host describes with the sibling commands' detection
  wording (omission no longer promises github.com), name the real
  bucket (`resolved`) in the review skill's Aone dedup note, and scope
  the design doc's remaining-unbacked claim to its own section.

* test(review): pin the Aone dedup seams the round-2 review named (#9627)

Four mutation-verified pins on the existing Aone backing, each closing
a round-2 Suggestion:

- classifyAoneChecks: the continue-scan cell of aoneCheckState — an
  unrecognized value in an earlier key beside a recognized verdict in a
  later key reads the verdict, not pending (a first-present-key mutant
  now fails)
- classifyAoneChecks: a context-keyed FAILED gate carries its name —
  the passing context-keyed case pinned nothing because passing gates
  never collect names
- both comment mappers: `note` beats `body` when BOTH keys are present
  (`??` does not coalesce `body: ''`, so an inverted priority would
  blank every recognition signal and re-post the whole review)
- aoneCommentToPresubmitComment: parentNoteId maps onto
  in_reply_to_id, including the absent-stays-unset half

No source changes; each pin fails under its named mutant and passes on
the current code.

* test(review): pin the five Aone seams the round-3 review named (#9627)

* fix(review): align Aone comment reads with measured a1 facts (#9627)

* fix(review): read fully-dropped Aone checks array as pending, not all-clear (#9627)

* fix(review): match SKILL.md self-PR wording to the revert-guard test

The merge resolution reworded the self-PR note to "matched against the
'a1 auth whoami' account", but SKILL.test.ts's revert guard (#9616, #9627)
pins the exact phrase "the MR author is matched against 'a1 auth whoami'".
Restore the pinned wording (semantics unchanged) so the bundled-skill test
passes.

* fix(ci): record qwen-autofix.yml's actual size in the workflow ratchet

The workflow-size ratchet failed on this PR: qwen-autofix.yml is 397656
bytes but .size-baseline recorded 392111 (5545 over, allowance 4096).

The oversize was inherited from main, not introduced here: main's ratchet
commit (a5d77eb8, #9677) shrank qwen-autofix.yml to 397656 but set the
baseline to 392111 — 5545 bytes below the file's actual size at that very
commit. This branch carries main's file unchanged (byte-identical), so its
CI is the first to trip the mismatch.

Growth is real in the sense that the file genuinely is 397656 bytes; per the
ratchet's own guidance ("if the growth is real, bump the number and say
why"), record the actual size so the ratchet measures future drift from
reality. The Post Coverage Comment failure is downstream of this (the Test
job exits before uploading the coverage artifact).

* fix(review): keep the pipeline's own pathless Aone summary out of the blocker index (#9627)

---------

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-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-23 04:50:14 +00:00
Shaojin Wen
7f2c4416b3
docs(review): name the axis when two of them both call it "rank 3" (#9759)
The trim-rank move gave `trim` a rank 3, and the file already used a bare
"rank 3" for the `keep` default on the last-resort cut's axis. The two mean
opposite things — `trim: 3` is the LAST rank the ladder sheds, `keep: 3` is
the FIRST thing the cut spends — so a reader landing on the cannot-tell
block's "Deliberately untagged (rank 3, spent first by the last-resort cut)"
now reads it against a comment three hundred lines up saying rank 3 goes
last.

Every mention of the number on either axis now says which axis it is on:
the cannot-tell block states the collision outright, and the four trim-side
mentions and the one keep-side test comment are qualified. The `keep`
comment that already disambiguated itself ("No `trim` rank rides here") is
unchanged.

Comments only, no behaviour. Raised by the review as a deferred, non-blocking
item; taken now because it is the same class of drift the previous commit
closed, and shipping the ambiguity would have seeded the next one.
2026-08-23 02:04:18 +00:00
Shaojin Wen
72d3a845f7
fix(review): count a fix-induced re-report as first-time work (#9744)
* fix(review): count a fix-induced re-report as first-time work

Closes #9674.

A carried id has meant two different things since the fix-induced
disposition shipped, and the volume trend's first-time count read both as
re-posts. One is a re-post: a finding re-asserted under the id it already
had. The other is a new defect wearing the id of the entry whose fix
produced it, carried deliberately so the author reads one thread per
churning site instead of a new one every round. Counting that as a re-post
made the trend understate new work exactly where the loop was creating the
most of it — measured on the pull request that introduced the disposition, a
round that newly identified six defects and re-reported four of them under
earlier ids recorded a first-time count of two.

Neither count moves. They measure different things and both readings are
correct, which is why the two reconciliations the issue rules out stay ruled
out: excluding carried-id re-reports from the census would put the attributed
count outside it and every such census would be refused as impossible, and
counting them as first-time posts wholesale would tell the trend a
re-assertion is new work. What was missing is the distinction itself, so the
comment now carries it: a fix-induced re-report is marked, and the reader of
drafted comments passes that through to the count.

The marking sits after the id and its separator, never inside the id
grammar. That grammar is shared with the ledger's own carry, so widening it
to swallow a parenthetical would put a finding's identity on the same regex
as a model-written adjective — a spacing the wider grammar failed to
anticipate would stop matching the id and silently renumber the finding.
Read after the id, nothing about the token can cost it, and the reading is
correspondingly lenient about case and spacing because it governs only
whether a comment counts as first-time work. An unrecognised marking leaves
the draft counted as a re-post, which is what every round did before this
existed; a marking wrongly added to a still-stands is the expensive
direction, so the skill restricts the token rather than offering it as a way
to flag any carried finding as interesting.

The token is stripped from the claim before it reaches the work list. Left
in, it would ride into the next round as part of the text Step 6 re-locates
the claim by and the status table prints — machine vocabulary about how to
count one round, outliving the round it described. Beside no id it is
ordinary claim text and survives, because there is no entry there for it to
qualify and editing a finding's own words on the strength of a word it
opened with is not this token's business.

* fix(review): resolve orphaned readback doc and record the fresh-count seam (#9744)

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

* fix(review): name the zero-prev masking round in the fresh-count seam note (#9744)

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

* docs(review): stop the blocker's docblock forbidding what this PR ships

Three passages written when a carried id could only mean one thing now
forbid the rule this branch adds. The blocker's docblock rules out
"counting them as first-time POSTS" as one of two reconciliations that must
never be made; the skill's census paragraph says the volume trend is the
count "where a carried id is a re-post"; and a test comment restates the
same premise. Each was true before a fix-induced re-report could be marked,
and each now tells the next reader to undo the code beside it.

The distinction the passages were protecting is real and stays. What they
ruled out was reading a carried id as first-time work BY INFERENCE, which
would count every re-assertion of a standing finding as new work — still
wrong, and still what `isFreshDraft` refuses. What this branch added is
narrower and is not an inference: the round marks the re-report, and only a
marked one counts. An unmarked carried id is a re-post to the trend exactly
as before, so the two counts still diverge by design; what is gone is the
premise that a carried id can mean only one thing.

The census paragraph gains the consequence that follows for whoever writes
the comment: a fix-induced finding counted in the census but left unmarked
in the body is counted by neither number.

Prose only — no logic, no test assertion changes. Reported twice by the
review as a deferred finding and left standing under the code-age rule,
which is correct as a posting decision and not a reason to leave a
contradiction in the file.

---------

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-23 01:57:19 +00:00
Shaojin Wen
ec8a8a1a97
feat(review): back pr-context on Aone Code targets (#9621)
* feat(review): back pr-context on Aone Code targets

pr-context was the one read subcommand still gh-direct, so every Aone
run was forced context-unavailable: the verdict capped at COMMENT (the
wired a1 approval could never fire), Agent 0 skipped, and the machine
ledger never recovered from posted summaries. Route it through the
platform reader with a normalized context bundle; Aone serves it from
mr view + the flat comment list (thread comments carry the ledger),
GitHub's implementation is an extraction of the existing calls — its
output stays byte-identical. The forced cap leaves the Aone write path
for parity with GitHub's state-claim handling, and the refetch commands
a context file emits bake --pr on Aone, where comment bodies are
addressed per-MR.

* fix(review): keep Aone ledger carriers out of the blocker re-check (#9621)

On Aone this pipeline's own round summaries are path-less comments, so
they ride pr-context's issue channel, where their visible
**[Critical]** lines self-promoted every prior Critical-bearing summary
into "Blockers to re-check" — rendering each prior Critical three
times (beside the ledger section and the inline roots that own the
same findings) and spending the section budget on the pipeline's own
prose until genuine human blockers degraded to snippets. Exclude
bodies carrying the ledger marker from issue-channel promotion and the
stdout count, strip the marker out of the settled snippet, and switch
the pr_number guard to the canonical isPositivePrNumber so 0x10/5.
spellings cannot fragment side-file continuity. Pin the witnesses the
round's findings name: the guard, args.host forwarding, the
issue-kind --pr refetch branch, the account-first author keying, and
the GitHub test suites' independence from the cwd-origin probe.

* fix(review): refuse pr_number spellings that do not round-trip (#9621)

isPositivePrNumber alone admits two spellings whose Number() value does
not round-trip to the raw string: leading zeros (007 fetches 7 but the
raw string labels the heading and the prev-ledger side file, so a later
7 run reads a different side file and the round counter restarts) and
digit strings above Number.MAX_SAFE_INTEGER (Number() silently rounds
them, fetching a different PR than the labels announce). Add the
safe-integer and no-leading-zero conjuncts — matching fetch-pr's
[1-9]\d* rule — so every admitted input satisfies String(Number(x)) === x.

Also pin the witnesses the round-2 review names: the commit_id
round-trip through the GitHub reader and toRawReview into the persisted
side file (both spreads were unwitnessed), the stale force-applies
comment in submit-aone.test.ts the cap removal outdates, and the setup
batch's Aone carve-out for the unbacked comment-status call.

* docs(review): align Aone docs with the landed no-ancestry anchor rule and comment-status skips

D6 described the AGit-Flow anchor as inert until the incremental rule
landed, but that rule (#9630) merged while this branch was in flight —
anchors now delta-scope Aone re-reviews. SKILL.md's comment-status
section and Step 6's report-existence guard now name the Aone skip the
setup batch already carries, so no path sends an Aone run at the
unbacked command or at a report that was never written.

* docs(review): annotate #9616 as landed and define the report-less re-check rule

The out-of-scope list still read self-PR detection as open work although
#9629 shipped it into this branch's merge base — annotate it like the
sibling #9618 entry. Step 6's report-existence guard pointed report-less
runs at a re-derivation the skill never defines; replace it with the
explicit rule: no per-thread status routing, no hand-derived substitute,
rule from the code at the reviewed commit, cannot-tell over a guess.

* fix(review): route the context head through aoneHeadSha and close the round-5 findings

getReviewContext read sourceBranch raw while every other head read trims
— a padded server value diverged the context file from the rest of the
run (phantom-drift shape). getCurrentUser now honors the seam contract
on the anomalous whoami shapes instead of leaking untagged throws and
non-string accounts. Step 6's report-less rule no longer contradicts
the comment-status failure contract: runs where the command ran and
failed keep the "re-derive if needed" fallback. The Aone paragraph
names comment-body among the backed reads, and witness tests pin the
identity gate's carriers key and the head normalization.

* fix(review): shape-check the Aone comment listing in getReviewContext

a1 can answer repo mr comment list with an exit-0 a1.error/v1 error
object (backend auth failure or client timeout — measured by cleanup's
a1CommentList on the identical payload). Without a guard the object
survives the ?? [] coalesce and .filter throws an untagged TypeError,
losing the envelope's actionable message at exactly the recoverable
moment. Guard as the provider family already does and surface the
cause; witness tests pin both envelope shapes (mutant-checked).

* test(review): pin getCommentBody's body-field fallback (mutant-checked)

* fix(review): union resolved comments into the Aone context bundle

The default comment list excludes resolved comments (measured by the
cleanup audit) while GitHub's REST fetches include them, so a resolved
blocker/marker root never reached the re-check walk or the fail-closed
identity gate. Union the default and --resolved listings as the audit
does, dedupe by id, fail closed on either listing's error envelope, and
disclose the residual that resolved replies stay invisible; witness
tests mutant-checked.

* fix(review): serve resolved comments and guard the envelope in getCommentBody

getCommentBody queried only the default comment list while the context
bundle it serves refetches for unions in resolved comments — a resolved
id named by a truncation note threw "not found" every time, and an
exit-0 a1.error/v1 envelope threw an untagged TypeError that lost the
actionable message. Extract the shape-checked default+resolved union
helper and read both sites through it; witness tests mutant-checked.

* ci: correct qwen-autofix.yml size baseline to its actual post-migration size

#9677 shrank qwen-autofix.yml from 431526 to 397656 bytes (prose moved to
the design record) but recorded the baseline at 392111, 5545 below the
file's own post-change size, so the first PR to run the ratchet tripped it.
This branch introduces zero growth to the file (byte-identical to main);
the bump aligns the baseline with reality. No workflow content changes.

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-23 01:10:32 +00:00
qqqys
98fa2e9770
feat(cli): enable dynamic workflows from a settings key (#9098)
* feat(cli): enable dynamic workflows from a settings key

`ConfigParameters.workflowsEnabled` is declared, defaulted, and read by
`Config.isWorkflowsEnabled()` — but `loadCliConfig` never writes it, so no
setting has ever reached it. The only way to turn dynamic workflows on is
the undocumented `QWEN_CODE_ENABLE_WORKFLOWS=1`, which has to be exported
in every shell that launches qwen. AGENTS.md names this shape directly: an
optional field that is declared and read but never set by any caller is a
dead switch.

Add `tools.workflowsEnabled` to the settings schema and populate the field
from it. Precedence is unchanged and still resolved in core:
`QWEN_CODE_DISABLE_WORKFLOWS` beats everything, then
`QWEN_CODE_ENABLE_WORKFLOWS`, then the setting. Because `settings.merged`
already folds the System scope, an operator gets a fleet-wide force-off
with no extra code.

`requiresRestart` is load-bearing rather than decorative: the Workflow tool
is registered once while the tool registry is built, `/workflows` is gated
when commands load, and keyword steering resolves at startup — so a
mid-session toggle would leave the dialog claiming the feature is on while
the tool is absent from the registry.

The setting description also disambiguates it from the unrelated
`experimental.sessionWorkflow` plan-and-review view, which shares the word
"workflow" and would otherwise be easy to confuse in the settings dialog.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(cli): clarify workflow feature controls

* test(cli): cover workflow command gating

* fix(cli): restrict workflow opt-in scope

* fix(cli): keep workflow opt-in user-owned

* test(cli): cover workflow system scopes

* refactor(cli): drive workspace-restricted settings from one list

R4-3: the restricted set was hand-maintained in three parallel places — a
per-key warning block, the condition in `stripWorkspaceRestrictedSettings`,
and that function's destructure. Adding one restricted setting needed three
synchronized edits, and either omission is silent: forgetting the warning
discards a workspace value with no diagnostic, forgetting the strip honors a
value the warning says is ignored.

`WORKSPACE_RESTRICTED_SETTINGS` is now the single source, and the warning
loop and the strip both derive from it. It lives in `settingsUtils.ts`
rather than `settings.ts` because `settings.ts` already value-imports that
module — defining it there and importing it back would close a runtime
import cycle.

R4-2: `tools.workflowsEnabled` is the first setting that is both
`showInDialog: true` and stripped from Workspace scope, so the dialog
offered a toggle that silently never took effect — it renders from the raw
scope file, so it kept showing the value it wrote while the feature stayed
at its merged value, leaving a dead entry in the repo's .qwen/settings.json.
`getDialogSettingKeys` gained `excludeWorkspaceRestricted`, which the dialog
passes when the selected scope is Workspace. The scope comparison stays in
the component so settingsUtils keeps its type-only dependency on settings.ts.
Unlike `showInDialog: false` (what the two pre-existing restricted settings
use), the setting stays visible and editable under the scopes that honor it.

Verified: settings 169/169, settingsUtils 85/85, BuiltinCommandLoader 13/13;
packages/cli typecheck clean. Mutation-checked both ways — forcing the
filter off fails 1 test, dropping a key from the list fails 3.
SettingsDialog.test.tsx's 23 failures are pre-existing and environmental:
identical counts on upstream/main and on this branch before the change.

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

* fix(serve): reject workspace-restricted settings at the daemon API too

R8-1. The workspace restriction stopped at the TUI dialog. `stripWorkspace
RestrictedSettings` drops these keys before every merge, so a workspace-scope
write through the settings API persists a committable dead entry into the
repo's `.qwen/settings.json` and answers 200 + `requiresRestart: true` while
the feature never turns on — GET then reports `workspace: true` beside
`effective: false`, and the warnings channel carries only `corrupted`, so the
client never learns the write was inert. Exactly the trap the SettingsDialog
comment in this same PR says it eliminates, one layer over.

`tools.workflowsEnabled` is the first workspace-restricted key with
`showInDialog: true`, which is what puts it in `getDialogSettingKeys()` and
therefore in `getAllowedKeys()` — the two pre-existing restricted keys are
`showInDialog: false` and never reached the API.

Both POST handlers now call one shared `rejectWorkspaceRestrictedWrite`,
answering 400 `workspace_restricted_setting`. One helper rather than two
copies, for the reason the previous commit collapsed the warning/strip pair.
User scope is untouched — that scope honors the key, and a guard that reached
it would kill this PR's whole enablement path.

Verified: workspace-settings 22/22, settings 169/169, settingsUtils 85/85.
Mutation-checked three ways — dropping either call site fails a test, and
widening the guard past workspace scope fails the user-scope test.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
2026-08-23 00:33:51 +00:00
qqqys
bd247128fd
feat(goal): account the tokens a Goal spends (#9583)
* feat(goal): account the tokens a Goal spends

A Goal reported how many turns it had run and how long it had been active, but
never what it cost. That is the number a user needs to judge whether a long
autonomous run is worth continuing, and the one every future limit has to be
expressed in — a budget cannot be enforced against a figure nobody keeps.

`GoalRecord` now carries `tokensUsed`, summed across the Goal's turns by
`reduceGoalTurnFinished`, and `get_goal` reports it in the unpermitted
`lastGoal` summary beside the turn count.

The figure comes from the chat recorder, which already receives every assistant
turn's usage stamped with the Goal permit that produced it. Attribution is
therefore settled where the spend is recorded rather than reconstructed
afterwards from session totals: a user turn interleaved with an autonomous run
belongs to no Goal turn, and a resumed session's replayed history is not a Goal
turn's spend either. The runtime asks the ledger for one turn by id when that
turn finishes, which is also why the accounting needs no coordination with
session swaps.

A runtime with no ledger, a ledger that throws, and a turn that made no model
calls all bill zero rather than guessing, and none of them fails the turn.
Goals recovered from a transcript written before the field existed restore with
zero spend. No limit is introduced here — this only counts.

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

* test(goal): cover recorder token accounting

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
2026-08-22 18:48:15 +00:00
Shaojin Wen
f829a02896
feat(review): validate Aone inline anchors against the captured diff before posting (#9634)
* feat(review): validate Aone inline anchors against the captured diff before posting

Aone Code performs no server-side anchor validation — a controlled probe
(scratch MR 29427547, a1 v0.2.51) proved any --line integer posts, and
an old-side number silently lands on the same-numbered new-side line.
The old side cannot be anchored at all, and file-level comments drop
their path.

Pin the removed-line semantics for the Aone write path: submit's Aone
branch now validates every well-formed inline anchor against the
review's captured diff before posting. An unanchorable Critical is
relocated into the summary body, an unanchorable Suggestion discarded
and counted — the GitHub 422-recovery dispose, performed in code — each
disclosed in the terminal. A missing captured diff refuses the whole
post; malformed shapes (missing path/line, reversed range,
renders-as-nothing) keep their consistency-gate refusals, and a garbage
state.bodyCriticals stands the gate down so compose's pinned refusal
fires. The GitHub path is untouched — its server performs this
validation.

Issue #9615

* fix(review): reject unpostable anchors and unify the Aone gate's shape refusals

- validateNewSideAnchors now rejects the input domain (fractional/zero/negative
  lines and reversed ranges) before the hunk scan, so its verdict can no longer
  certify an anchor the zero-validation Aone platform would post silently wrong.
- Extract the consistency gate's per-comment shape checks into one shared
  predicate (commentShapeProblems) read by both the loud refusal and the Aone
  anchor gate, so a shape the gate disposes is never a refusal the operator
  misses (open fence, start_line-without-side). The path check becomes a type
  check, closing truthy non-string paths that reached the write seam unvouched.

* fix(review): generalise the Aone gate's stand-down and harden its relocated entries

Round-2 review fixes for the Aone anchor gate:

- The stand-down now keys on ANY degrade that touches the payload and
  covers every compose-owned garbage shape: bodyCriticals that is not an
  array of strings, or a suggestionsDiscarded compose's counter refuses.
  The countability test reads compose's OWN acceptance table (toCount,
  exported as the total tryToCount), so the gate's merge and compose's
  counter can never drift — an integer-but-not-safe count now merges
  instead of silently dropping the gate's discards.
- The relocated entry's claim extraction strips a leading marker RUN
  (fixpoint, like every other strip) and treats a fence-delimiter claim
  line as absent — both shapes used to leak raw markers or junk
  delimiters into the posted summary-body blocker line.
- The gate keeps the model-authored comment indices through its removal
  (and floor enforcement keeps them through its own), so the consistency
  gate's refusal names the culprit in the model's payload JSON instead of
  a renumbered position the re-compose loop cannot act on.
- A --dry-run with a missing capture no longer exits 3: it writes
  nothing, so it skips the gate with a disclosure and reports
  wouldPost: false (reason: aone-diff-missing); the exit-3 refusal stays
  reserved for the real write.
- The MULTI_DIFF fixture's second hunk header becomes byte-exact git
  output (@@ -20,0 +22,2 @@, probed against git itself).
- The design doc gains the gate-relocation doctrine (relocated entries
  deliberately inherit the model's own tag-exemption treatment), the
  dry-run carve-out in the failure-shape table, and the corrected
  fence/one-line-channel claim.

* fix(review): close the anchor-gate witness gaps and a footer-leak in the relocated entry

Gap-fill on top of the round-2 gate hardening:

- The relocated entry's claim extraction strips the canonical footer
  FIRST: with an empty claim line (a marker-plus-separator-only body),
  the separator strip eats the newline+colon and the extraction falls
  THROUGH into the appended footer's first line, posting it as the
  claim. Witness added for the placeholder shape.
- Pin the multi-line relocation entry CONTENT (it must cite the claimed
  end line, not the start — the start sits inside the hunk and looks
  fine) and the disclosure naming it.
- Witnesses for the remaining mutant-tested gaps: a range whose start
  sits outside every hunk and end inside (the startLine mapping), the
  dry-run compose parity (preview composes from the gate-corrected
  payload), the suggestionsDiscarded 0 merge boundary, the empty-path
  shape (loud refusal, never a gate disposal), a declared LEFT
  start_side without a start_line, and the equal-boundary range
  (start_line === line, a shape GitHub itself produces).
- The routing suites run from a per-test fixture cwd, so the
  captured-diff seeding and its cleanup can no longer overwrite or
  delete a same-numbered live capture in the real vitest cwd.

Issue #9615

* fix(review): sanitise relocated-entry paths and stand down over any compose-refused bodyCriticals

* fix(review): keep the anchor gate's captured diff when resetting the receipt state

The Aone receipt suite's beforeEach wiped the whole .qwen tree to start
from no receipt — deleting the captured diff the anchor gate needs along
with it. Every post then died at the gate's missing-capture refusal and
no receipt was ever written (ENOENT in the four receipt tests on CI).
Remove only the receipt file; the seeded diff survives.

* fix(review): close the anchor-gate entrances the review rounds demonstrated

Round-3 remediation of the review comments on the Aone anchor gate:

- R3-2 (structural): the BUILT relocated entry is now validated against
  compose's own ingestion (tryIngestBodyCriticals over the single entry)
  before the relocate is disclosed, and any refusal degrades the entry to
  the inert constant `finding — (no path):<line>` — the entrance space is
  unbounded model text and compose's acceptance is the authority, so a
  shape the enumerated guards never anticipated degrades the entry
  instead of refusing the whole post mid-degrade. The demonstrated
  entrance (a lone CR inside the claim: it passes the leading-fence
  guard, compose's CR normalisation then splits the entry and the second
  line leads with a fence delimiter) is covered by a witness.
- Ledger collision: the relocated entry flips to `<claim> — <path>:<line>`
  — the claim leads, so a carried id keeps position 0 and the ^-anchored
  ledger readback matches instead of silently renumbering a carried
  finding as new. Witness asserts the id survives the readback regex.
- R7-1: an explicit JSON null side/startSide reads as ABSENT (defaults
  to RIGHT), the model's idiom for an omitted optional field — never a
  declared old side. Unit and gate-level witnesses.
- R3-3: witness for the non-identity authoredIndices branch — the gate
  renumbers the array, floor enforcement keys on the post-gate array,
  and the remap drops the comment floor enforcement names.
- R4-2: the hostile-paths test gains the \r-bearing path (compose's
  ingestion normalises a bare CR to a line break — the same hostile
  shape as \n; the guard's \r half was unwitnessed).
- R3-5: the design doc states the carve-out — the non-RIGHT degrade runs
  for single-line comments only; a multi-line non-RIGHT comment keeps
  the consistency gate's whole-post refusal; null side is absent, not a
  declaration. The failure-shapes table splits the row accordingly.

Issue #9615

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-22 17:45:27 +00:00
jinye
2172721405
feat(cli): restore each daemon session onto its last selected model (#9687)
* feat(cli): restore each daemon session onto its last selected model

Idle detach currently rebuilds Config from settings.model.name, so session A picks up whatever model session B last switched to.

Fixes #9686

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): address session-model persistence review findings

- reader: always select the last assistant record into the restore read
  set so the legacy lastAssistantModel fallback still fires when a
  trailing chat_compression candidate excludes it from the resume read
- recorder: assign currentSessionModel before the awaited write so a
  rewind landing in the pending-write window re-anchors the new binding
  instead of the stale one
- reader/recorder: reject non-string session_model payload fields
  instead of crashing the restore path on malformed transcripts
- protocol doc: describe the session_model append as best-effort, not
  an unconditional consequence of a successful switch
- cli: import RUNTIME_SNAPSHOT_PREFIX/stripRuntimeSnapshotPrefix from
  core instead of duplicating the prefix algorithm locally
- tests: pin the isRuntime/baseUrl payload dimension, the prefix and
  route-mismatch false arms, the neither-field fallback, and regression
  coverage for the two fixes above

* fix(cli): keep daemon session-model restore from failing load

Pre-auth restore skipped the last-assistant fallback, and a recorded qwen-oauth binding could hard-fail load when cached credentials were gone.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): roll session-model auth retry back onto the settings route

Same-id baseUrl restores and runtime-only settings models were skipping or breaking the fallback, which made load fail on the recorded credential set.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): keep empty daemon sessions from creating a transcript

Recording the session model on newSession wrote a jsonl file before any user content, so close/delete/child-death left the id occupied and listing still showed the empty session.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): allowlist restored session-model routes against the registry

JSONL baseUrl is only a registry selector, so unknown hosts are dropped before switchModel. Restore also keeps the last valid session_model payload instead of falling through a torn trailing record.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): retry session-model auth after same-id snapshot restore

The retry gate ignored runtime-snapshot identity, so restoring an implicit
registry route off a same-id snapshot looked unchanged and skipped rollback.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 16:33:36 +00:00
Shaojin Wen
6c742ec792
feat(review): ask each fix for its test, and rule on non-convergence (#9596)
* feat(review): ask each fix for its test, and rule on non-convergence

The review-fix-re-review loop is its own largest customer. Provenance
analysis of six multi-round takeover pull requests attributed each
post-first-round finding to the commit that introduced the line it anchors
on: roughly a third were introduced by the fix round immediately before the
review that found them, overwhelmingly as a guard or branch with no test of
its own. That measurement produced a fix on the loop side, where it reaches
exactly one fixer. Most pull requests are not fixed by a bot the review can
configure, and whoever does fix a finding reads only the comment.

So the acceptance criterion moves into the finding and into the posted
comment. A finding whose suggested fix adds a guard, a branch, or a behavior
now names the test that must go red if the fix is removed, and the comment
asks for the mutation that proves it. The criterion never gates reporting: a
finding whose fix cannot be pinned is filed anyway, because a bar on
reporting would trade rounds for missed defects, and the evidence rule that
governs what confirms a finding is a separate one.

Second, a round stops renumbering its own churn. A new defect the reviewer
can trace to the change that answered a previous entry is re-reported under
that entry's id rather than taking a fresh one, so the author reads one
thread per site instead of a new one every round, and the cross-round work
list stops spending an id per round on a site the loop is circling.
Attribution is bookkeeping and never a posting decision; it applies only
when the new defect is at least as severe and as confident as the entry it
carries; and anything it cannot trace takes a fresh id, which is what every
round did before.

Third, that attribution produces a count, and the count is what ends a loop
the review cannot close by filing more findings. Each round hands over how
many findings first appeared and how many of those it attributed to the
previous round's fixes. The command owns the threshold, carries the streak
across rounds, and files its own blocking finding on the second consecutive
round in which most of the round's new work was work the previous round
created. It reads the attributed count deliberately, not the count of
findings on newly pushed lines: a pull request whose author pushed a feature
between rounds created none of them out of the review, and a bar built on
the looser number would block a pull request for growing.

* fix(review): cross-check the churn census and harden the streak's edges

Round-1 review findings on the non-convergence mechanism:

- Refuse a census whose fresh count exceeds everything the round
  reports (drafted comments, body Criticals, deferrals). The census is
  the model-written half of the trigger; this one-sided bound is the
  cross-check it gets before it can arm the streak, so a round that
  reported nothing can no longer file the blocker on the model's
  say-so alone.
- persistRecoveredLedger's anonymous-advance branch now drops
  churnRounds/fresh/induced with the other round-specific facts: a
  streak re-dated across a round this account never ran would arm the
  blocker one round early and discard the foreign winner's own streak
  state. The plain recovery path round-trips them, and both seams are
  now pinned.
- SKILL Step 6's fix-induced rule caps re-reports at one per original
  id per round — two same-id entries are a duplicate id, and the
  artifact validator refuses the round's findings whole.
- Reword the posted blocker and its docblocks to what the arithmetic
  actually does: the bar is half-or-more (not "most"), it keys on the
  attributed count (not findings on new lines), and the streak counts
  rounds against the bar — rounds that could not measure carry the
  count — rather than calendar-consecutive rounds.
- parseLedger clamps a recovered streak to the marker's own round:
  the streak counts rounds inside the round it rides, and an
  unclamped forged streak inflates the posted ordinal past everything
  the pull request ever ran.
- Witness pins for the gaps the reviewers probed: the >= filing
  condition at streak 3, ordinalSuffix past 2 (rd/teen-th/st),
  CHURN_MIN_FRESH from both sides, and the full corrected blocker
  text.

* fix(review): strip foreign churn state at the recovery seam

Round-2 review findings on the non-convergence mechanism:

- recoverLedger now strips churnRounds/fresh/induced from a foreign
  winner beside the anchor. Left riding, any account that can submit a
  review could plant a streak through the identity-known write path and
  trip the blocker one honest census later on a pull request that never
  churned; the anonymous-advance drop stays as defence in depth.
- A below-minimum census carries the streak like an absent one — three
  findings are rounding, not a trend — so a pull request alternating
  above-bar rounds with small ones still reaches the filing bar; the
  filing condition takes back its explicit above-bar guard, which the
  carry makes reachable again.
- Round 1 refuses a census outright: with no predecessor nothing can be
  fix-induced, symmetric with the round-0 streak guard.
- SKILL Step 6 counts fresh over the three reporting channels the module
  cross-checks — deferrals in, terminal-only and unanchorable drafts
  out — and Step 4's aggregate template gains the Fix witness slot
  Step 6 already names.
- Witness pins for what the reviewers mutated: the seam strip and its
  round trip, the three-channel sum on both non-drafted channels, the
  carry and its guard, the round-1 refusal, the finder brief's
  fix-witness format, and the aggregate slot.

* fix(review): restore own churn state at the union, clamp the side-file streak

* fix(review): name the churn group once, and part the two fresh counts

The convergence state was hand-enumerated at three production seams while
the volume group beside it documents a single shared list as the remedy for
a bug that already shipped once there — a field kept at one seam and shed at
the other. Nothing reds on a missed enumeration until a fourth field is
added, and the two ways to miss it are both silent: left in the side file
after the counter advances past the round it describes, or dropped from the
restore that protects this account's own data on a merged round. The group
now has one name, and the strip, the restore and the counter-advance branch
all read it.

The other half is a collision of words, not of arithmetic. One posted body
carried two counts of what a round did for the first time: this mechanism
counts DEFECTS newly identified, and the volume trend counts inline comments
POSTED for the first time. They legitimately differ — a fix-induced defect
re-reported under the id it came from is new work here and a re-post there —
so a round can newly identify six defects while posting two first-time
comments, and both numbers are right. Written as "findings first filed"
beside "reported for the first time", neither could be trusted. The blocker
now says "defects newly identified", and both the module and the skill
record why the two must not be reconciled by changing either one: excluding
carried-id re-reports from the census would put the attributed count outside
it and every such census would be refused as impossible, while counting them
as first-time posts would tell the trend that a re-post is new work.

Four witness pins close the gaps the last rounds left. The census
cross-check summed three reporting channels but every arm populated one at a
time, so a non-additive reduction shipped green and would refuse a census on
the ordinary shape of a round with body blockers beside inline findings. The
same-round union's churn restore had no pin through the persist seam — the
existing foreign-winner arm is cross-round, where the state is already gone
and a second drop is a no-op. The anonymous recovery walk's churn strip was
unpinned, so a refactor gating it on a known identity would let a foreign
streak ride an identity blip into the side file. And the finding format's
N/A exemption was pinned only up to its prefix, so deleting the clause that
keeps the criterion from becoming a bar shipped green in the copy that
actually reaches the agents.

* fix(review): name fix-induced in the ruling, pin the census clauses, drop the unread marker pair

* fix(review): carry the churn streak through unmeasured rounds

The carry contract says unmeasured rounds carry the count, and two seams
broke it for the cumulative streak while handling it correctly for the
per-round volume:

- The union restored own churn state only when the own marker described
  the SAME round as the winner, so a strictly NEWER foreign winner
  silently zeroed this account's standing streak — on a PR two accounts
  alternate on, neither ever reached the filing bar. The streak now
  restores across the round gap; only pickVolume stays same-round gated.
  No foreign state enters: the winner's streak is stripped at the
  recovery seam, and the restore spreads only the own marker's state.

- The anonymous-advance branch dropped the streak from this machine's
  own side file, so an identity blip (the gh api user failure the branch
  already anticipates for the volume) reset a standing claim; repeated
  blips kept the blocker unreachable on exactly the churning PRs. The
  drop rationale clauses do not apply — the winner's streak cannot reach
  this seam, and carrying arms nothing early because filing still needs
  THIS round's own above-bar census. The streak now carries, matching
  the sibling recovery-threw state and the filed blocker's own body.

Also part duplicate-dropped findings from the census `fresh`: they
restate defects an earlier round identified (the duplicates paragraph
discloses the confirmation; it is not a fourth reporting channel), and
counting them let the module refuse a census the rule as written
licensed. The exclusion is now explicit in SKILL's NOT-counted list and
pinned by the census contract test.

Witnesses: the cross-round persist test now asserts own streak restored
(1) and planted streak gone (never 4); the anonymous-advance test
asserts the streak survives the counter advance. Mutation probes on
each guard red when removed, green when restored.

* fix(review): part "reset" from "not recorded" at the churn seams

Two blockers landed on the same seam pointing opposite ways: one that the
identity-known write drops a standing streak, one that the anonymous-advance
branch keeps a stale one. Applying both suggestions as written would have
reverted a seam that had already been reversed once, so neither is applied
directly; the shared cause is fixed instead.

That cause is an ambiguity. A round measuring below the bar resets by
stamping no streak at all, so "no churn state" is written by a reset and by a
marker that was never read, and the two paths resolved it in opposite
directions. Recovery now reports whether an own marker was actually READ —
distinct from whether an own review exists, which is the case the corrupted
marker falls into — and the seams read that instead of guessing from absence.

The identity-known write carries the file's streak only when no own marker
was read: nothing authoritative said reset, so the file still holds the last
state this account certified. When one was read, it has spoken in whichever
direction and the write leaves it alone, so a real reset still lands.

The anonymous-advance branch sheds the streak with the volume. The argument
for keeping it was that a carried streak arms nothing early because filing
still needs the round's own above-bar census; that shows it is only USED
where a measured round finds it, not that it is still true there. With no
identity this branch cannot tell this account's own reset marker from a
stranger's, and carrying one lets a later census reach the bar a round early
with the blocker's own body claiming rounds that did not pass. Dropping costs
only the outage: the own marker stays on the pull request, so the next
identity-known recovery re-establishes the true streak.

The rule the two now share is one sentence. Carry while the state is known to
be ours and current; drop where it can be neither attributed nor dated.

Three smaller things fell out of checking the fix rather than the findings. A
carried streak is read through the ledger's own reader and clamped to the
round it is written beside, because this is the first path where bytes from
the file survive a write instead of being replaced by it. The anonymous whole
write now sheds the churn group as it already shed the volume, so that seam
defends itself instead of resting on an upstream strip, and the assertion
covering it was rewritten over a fixture that actually carries a streak — it
had been holding vacuously. And two guards that no mutation could redden were
resolved explicitly: one removed as an invariant of the strip above it, one
kept as defence in depth with its unreachability and its behavioural pins
named, because it sits on the exact axis the second blocker was about.
2026-08-22 16:24:04 +00:00
Shaojin Wen
079e22a914
feat(review): add temporal-reachability and incident-replay lenses (#9708)
* feat(review): add temporal-reachability and incident-replay lenses

Two blind-spot fixes measured on PR #9655's escaped P1 (a post-run
--capture that could not steer the run it documents, plus brief text
telling the witness to quote it as though it had):

- Agent 1c: reachability gains a TIME axis. A value produced after
  every decision it should influence is a record, not a mechanism;
  when documentation or workflow guidance treats the record as a
  mechanism, that is the Critical, with 'produced at X, needed at Y,
  Y precedes X' as the whole trace.
- Agent 0: a motivating incident narrated in the PR context is
  replayed step by step against the post-change workflow, regardless
  of closing-keyword formality. An unchanged outcome is a Critical
  even when the diff faithfully implements what its issue prescribed —
  an issue can prescribe a remedy that never reaches its own observed
  failure. An empty closing set no longer empties the replay duty.

* fix(review): pin the new lenses and give the replay an enforceable contract

Round-1 review feedback on this PR, all five findings addressed:

- R1-1/2/3: the three added passages were unpinned — a future deletion
  shipped green. Weld-style pins added in agent-prompt.test.ts (the
  enumeration-trap precedent), covering the replay duty, its un-gating,
  the TIME-axis paragraph, the trace format, and the verifier clause.
- R1-4: the empty-scope return now carries a fourth evidence item — the
  replay's outcome (the step that changes, or the reason none does), or
  an explicit statement that the description narrates no incident — so a
  skipped replay never reads identically to a performed one.
- R1-5: the orchestrator contract buried the lens's product in the exact
  case it was written for — SKILL.md forbade falling back to the PR
  description and the verify brief downgraded fidelity findings lacking
  issue evidence to low confidence (terminal-only). Carve-outs added in
  critical rule 4, the Step 2 context paragraph, and the verify brief: a
  replay finding quotes the PR's own narrative as its evidence, judged
  as the PR's claim about what the change prevents, not as ground truth.

* fix(review): route the no-step-changed replay outcome to a finding, never the receipt

Round-2 review feedback, all four findings addressed:

- R2-1 (Critical): round 1's fourth evidence item routed the replay's
  no-step-changed outcome INTO the scope-empty receipt while the bullet
  above mandates it as a Critical — two mutually exclusive return
  shapes, and a receipt contributes nothing to the verdict, so the
  mandated Critical could dissolve. The contract now routes explicitly:
  no step changed = a findings return; the receipt carries only the
  benign outcomes (the step the replay saw change, or an explicit
  statement that the description narrates no incident).
- R2-2: four load-bearing clauses pinned — the replay's Critical
  severity, 1c's record-as-mechanism severity condition, the
  distinguishability sentence, and the verifier's no-downgrade clause.
- R2-3: the orchestrator-facing copies of Agent 0's return contract
  (the whiff-check parenthetical and the roll-call example) updated to
  the new shape, so a skipped replay cannot pass as the old three-item
  receipt the prose told the orchestrator to accept without relaunch.
- R2-4: SKILL.test.ts revert guards for both SKILL.md copies of the
  incident-replay carve-out, following the rule-4 guard's pattern.

* fix(review): complete the receipt example and pin the last unpinned clauses

Round-3 review feedback, all three findings addressed:

- The roll-call example restores the 'not a bugfix' evidence item the
  round-2 rewrite dropped — it now models all four receipt items, so an
  orchestrator shaping its Step 6 line on it cannot certify scope-empty
  for a bugfix PR without that determination asserted.
- The orchestrator-side copy of the R2-1 routing rule and the roll-call
  line are pinned in SKILL.test.ts's carve-out guard: reverting either
  restored the pre-R2-1 receipt standard while every brief-side pin
  stayed green.
- The TIME-axis pins gain the definition clause ('a record, not a
  mechanism') and the two-moments method — without them the severity
  rule names a split nothing defines.
2026-08-22 15:47:35 +00:00
Bob.qwencode
c10143a9c1
chore(release): v0.22.0 (#9736)
* chore(release): v0.22.0

* docs(changelog): sync for v0.22.0

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-22 15:23:02 +00:00
jinye
39378ac0a4
feat(serve): restore ask_user_question HITL on session load/resume (#9665)
* feat(serve): restore ask_user_question HITL on session load/resume

Keep a trailing unanswered question votable after daemon load/resume when --restore-ask-user-question is on, instead of closing it as a failed tool result.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(daemon): harden ask_user_question restore per review feedback

- acpAgent: defensive restore hint (no `!` lookup; accepts undefined
  session) + normalized id lookup on both cold return paths; session
  test doubles carry shouldHintAskUserQuestionRestore
- bridge: single maybeFireRestoreAskUserQuestionPrompt helper with the
  full admission-time busy predicate (pendingPromptCount +
  goalTurnActive), sync-throw try/catch, no-attached-client gate, fork
  suppression, and hasActivePrompt reflecting an admitted restore
  prompt; child-bound requests carry a suppress meta when the daemon
  already knows it will decline, keeping replay skip and re-hang
  aligned
- Session: restore prompt gated on the config flag; early bail before
  per-turn bookkeeping when history is not restorable; system reminders
  ride the post-answer message; restore turns no longer burn the
  active-todo reminder; a permission timeout on a restored question no
  longer persists the fabricated decline (transcript stays dangling for
  a later re-hang); continueLastTurn declines a restorable question;
  restorable detection reads peekLastHistoryEntry instead of cloning
  the full history
- history-replay-page: isInitialized() guard on the skip probe; dead
  paged-path skip wiring removed
- transcript-replay: skip set matches raw ids after dedup renames
- core: inline orphan-repair preserves the restored AUQ ids; the
  compression side query strips a trailing dangling functionCall; the
  CLI flag is honored only in ACP mode

* fix(cli): skip restore hint helper when the switch is off

Load/resume used to call shouldHintAskUserQuestionRestore on every Session, including test doubles that do not implement it. Short-circuit on argv first so the default-off path stays independent of the restore-only API.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 14:19:18 +00:00