mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-26 09:04:37 +00:00
4164 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
adda82a9e7 | Merge remote-tracking branch 'origin/main' into HEAD | ||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
37cedea5b2
|
feat(computer-use): replace built-in tools with bundled skill (#9856) | ||
|
|
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> |
||
|
|
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). |
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
f241c19ace
|
fix(core): support per-provider stream idle timeout (#9795) | ||
|
|
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> |
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
a369b4fac6
|
fix(cli): prevent input border overflow on resize (#8991)
* fix(cli): prevent input border overflow * fix(cli): harden border width invariant |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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
|
||
|
|
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> |
||
|
|
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 |
||
|
|
1e062a4d0f
|
perf(cli): raise VP scroll rendering to 60 FPS (#9681) | ||
|
|
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 (
|
||
|
|
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.
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |