Commit graph

7151 commits

Author SHA1 Message Date
qqqys
22bb5e8b9f
feat(core): require an explicit user opt-in before the model launches a workflow (#9806)
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
* feat(core): require an explicit user opt-in before the model launches a workflow

The Workflow tool description carried judgement heuristics for orchestrating
well, but no rule about when not to orchestrate at all. Read on its own the
prose is encouragement, and a run that can dispatch up to the per-run agent
cap is a large spend to enter on inference rather than on a request.

Prepend a gate above the existing guidance: do not call the tool unless the
user asked for multi-agent orchestration, with the five forms that count as
asking under this project's own triggers -- the `workflow` keyword, the
user's own words, a skill or slash command, a named saved workflow reached
through `workflow('<name>')` or `scriptPath`, and a resume. Upstream's
`ultracode` marker is deliberately not among them: it does not exist here,
and naming it would enumerate a trigger no user can pull.

The fallback path is the load-bearing half. Without an offer-and-ask route
the model reads a refusal rule as "refuse", and a user who would have said
yes never gets asked, so the text ends by telling it to say what a workflow
would fan out over and let the user decide.

The agent cap is interpolated from `DEFAULT_MAX_AGENTS_PER_RUN` rather than
pasted, matching the rest of the description.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDuRWMhKbqEYZXbbLhn3en

* chore: keep agent-core.ts as it is on main

The merge commit's pre-commit hook ran prettier over every file the merge
staged, not just the ones this branch changes, and reformatted a type union
in agent-core.ts. main's copy does not satisfy the repo's pinned prettier
3.6.1, so the hook produced a real diff in a file this PR has no business
touching. Restore main's bytes to keep the PR scoped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDuRWMhKbqEYZXbbLhn3en

* test(core): pin workflow opt-in contract

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
2026-08-24 17:39:04 +00:00
qqqys
bf123a375a
fix(goal): converge the three continuation prompts on one guarded contract (#9834)
* 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)

* fix(goal): converge the three continuation prompts on one guarded contract

Every automatic Goal turn now renders the same prompt in every host: the
runtime-supplied goalId, revision and objective as an escaped JSON data
block, framed as untrusted task data, under both anti-spoofing guard
lines, followed by a line stating the block supersedes any earlier
objective in the conversation.

Before this change the drift ran the wrong way. ACP and non-interactive
interpolated the raw objective into a synthetic user-role turn carrying
neither guard line; the TUI carried both guard lines but dropped the
objective, so the host that guarded most gave up information and the two
that guarded least were the exposed ones. None of the three escaped the
objective, so objective text shaped like a tag could break out of the
surrounding prompt.

The prompt input collapses to a single flat shape, so the variant
discriminant and its unreachable-default arm are gone. `<`, `>` and `&`
are escaped inside the serialized JSON so an objective cannot close the
data block or open one of its own.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-24 17:38:29 +00:00
qqqys
ac109c9dd3
fix(core): relax uniqueItems in function schemas (#9869)
* fix(core): relax uniqueItems in function schemas

* fix(core): preserve schema map keys when relaxing tools

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
2026-08-24 17:15:28 +00:00
Shaojin Wen
02f5214939
test(cli): skip unreadable-ledger run-ledger test under root (#9913)
* test(cli): skip unreadable-ledger run-ledger test under root

The test simulates an unreadable ledger with chmod 000, but root
bypasses permission bits, so the read succeeds and the append goes
through, failing the test on every root run. Skip it there since the
property is untestable without DAC enforcement.

Fixes #9909

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

* test(cli): extend unreadable-ledger run-ledger skip to win32

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
2026-08-24 16:44:27 +00:00
jinye
1fffa5108d
fix(acp-bridge): Disable permission timeout by default (#9933)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
npm cache producer / Save npm cache (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
Security Checks / Dependency CVE audit (push) Waiting to run
Security Checks / Secret scan (TruffleHog) (push) Waiting to run
* fix(acp-bridge): disable permission timeout by default

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

* chore: regenerate settings schema

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

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

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-24 16:08:44 +00:00
tlysanhuo
f7b936630f
fix(core): reject malformed Anthropic tool arguments (#9013)
* fix(core): reject malformed Anthropic tool arguments

Validate streamed tool input as protocol JSON and release tool calls atomically only after a trustworthy message stop. This prevents truncated arguments or open parallel tool blocks from leaking executable calls into retry consumers.

* fix(core): preserve max-token recovery for tool truncation

* fix(core): preserve interrupted Anthropic tool calls

* fix(core): recover empty max-token responses

* refactor(core): share stream retry predicates

* Merge main into tlysanhuo/fix-anthropic-tool-json

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: yiliang114 <effortyiliang@gmail.com>
2026-08-24 13:05:31 +00:00
Harjoth Khara
95bdd46241
fix(config): accept output.format "stream-json" in the settings schema (#8966)
* fix(config): accept output.format "stream-json" in the settings schema

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

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

Closes #8965

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

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

Apply the review's non-blocking suggestions:

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

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

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

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

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

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

Address the round-2 review:

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-24 12:45:51 +00:00
Yu Zhang
2dbe806204
docs(sdk): fix query timeout example signature (#9867)
Co-authored-by: zhangyu.34 <zhangyu.34@bytedance.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-24 12:40:05 +00:00
Yu Zhang
c216584e01
docs(core): fix goal judge timeout unit (#9861)
Co-authored-by: zhangyu.34 <zhangyu.34@bytedance.com>
2026-08-24 12:39:52 +00:00
Yu Zhang
703bb7a5cf
docs(core): fix read file paging default comment (#9863)
Co-authored-by: zhangyu.34 <zhangyu.34@bytedance.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-24 12:39:50 +00:00
qqqys
24db7f6ef2
feat(review): say when the approach, not the patch, is the open question (#9340)
* feat(review): say when the approach, not the patch, is the open question

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

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

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

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

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

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

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

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

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

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

* fix(review): validate approach signal evidence

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

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

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

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

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

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-24 12:27:26 +00:00
易良
d128998779
fix(core): accept quiet post-tool-result completions after retry exhaustion (#9196)
* fix(core): accept quiet post-tool-result completions after retry exhaustion (#9026)

Models that legitimately end a turn silently after a tool result (valid finish reason, no visible content) trip the NO_TOOL_RESULT_PROGRESS guard; the #7039 retry budget gives them four identical retries and then the error aborts the whole run — the dominant failure mode across ~390 headless rollouts on GPT/Gemini-family models (291/96 hits vs 0 on Qwen). Keep the retry-first behavior and, once the budget is exhausted, accept the quiet completion instead of re-throwing: the MAX_TOKENS variant stays fatal, a placeholder keeps user/model alternation well-formed when the accepted turn produced nothing, and the JSONL record carries the same text as history. Explicit per-attempt one-shot flag; continuation streams share the same acceptance through the shared retry wrapper.

* fix(core): scope quiet completion retry flag per attempt

* test(core): pin quiet completion retry edges

* fix(core): close round-1 findings on quiet-completion acceptance (#9026)

- consume the one-shot acceptance where the retry outcome is rescheduled,
  not where the attempt starts: re-arm the flag in the rate-limit /
  transport-replay / transport-continuation / reactive-compression
  continue branches whenever either invalid-stream budget is exhausted,
  and arm on any budget-exhausting invalid-stream type (the accept gate
  independently requires a tool-result continuation with no visible
  progress and a non-MAX_TOKENS/SAFETY/RECITATION finish)
- keep SAFETY/RECITATION-blocked quiet turns fatal: content-filtered
  continuations (OpenAI content_filter / Anthropic mapped to SAFETY)
  must not end silently with no user-visible signal
- record accepted quiet turns from one source: hoist acceptedTurnParts
  and build the JSONL record from the same parts history keeps, so an
  accepted turn whose only parts are non-text (inlineData/fileData) no
  longer desyncs transcript from history on --resume
- fatal-MAX_TOKENS test: attach the rejection assertion before advancing
  fake timers so the file's test command no longer exits 1 on an
  unhandled rejection
- regression tests: armed attempt surviving a transport replay, a 429,
  and a mixed final error type; inlineData-only accepted turn recording;
  SAFETY quiet turns staying fatal

* fix(core): close round-3 findings on quiet-completion acceptance (#9026)

R1-1: the invalid-stream retry branch now rearms the one-shot quiet
acceptance like the other four rescheduling paths, so an armed attempt
that fails with PROTOCOL_TAG_LEAK no longer strands the acceptance.
Arming is keyed to the transient bucket only: quiet completions surface
as a transient-type error, so a tag-leak-only budget exhaustion can no
longer arm prematurely and bypass the #7039 retry-first invariant.

R2: the accept gate keeps the whole content-filter finish-reason family
fatal (SAFETY/RECITATION/BLOCKLIST/PROHIBITED_CONTENT/SPII and the
IMAGE_* variants), mirroring mapGeminiFinishReasonToOpenAI's
content_filter grouping; the constants move into genai-compat's
FinishReason alongside MAX_TOKENS.

Also drops the dead contentText placeholder write (acceptedTurnParts is
the single source for the JSONL record and history push), parameterizes
expectStreamExhaustion so the fatal tests reuse it (clearing the
vitest/valid-expect lint failures), hoists chatWithRecorder to the
shared scope, pins RECITATION via it.each, and adds regression tests
for the tag-leak rearm, tag-leak-only no-arm, and the armed-continuation
rearm site.

* fix(core): keep IMAGE_OTHER fatal in the quiet-completion gate (#9026)

mapGeminiFinishReasonToOpenAI also groups IMAGE_OTHER into
content_filter; include it in CONTENT_FILTER_FINISH_REASONS so a
native-route IMAGE_OTHER block cannot be silently accepted either.

* fix(core): map Anthropic refusal stop_reason into the content-filter family (#9026)

R6-1: mapAnthropicFinishReasonToGemini had no entry for Anthropic's
refusal stop_reason, so it fell through to FINISH_REASON_UNSPECIFIED —
defined (passes the NO_FINISH_REASON throw), not MAX_TOKENS, and not in
CONTENT_FILTER_FINISH_REASONS. Once the retry budget was spent, the
armed attempt therefore accepted a provider refusal as a quiet
"(empty content)" completion, masking the safety decision the gate was
written to keep fatal. Map refusal to SAFETY alongside content_filter
so the existing content-filter family keeps it fatal.

Regression tests: converter refusal-to-SAFETY mapping (mutation-
verified) and a geminiChat armed-attempt test asserting
NO_TOOL_RESULT_PROGRESS exhaustion with no placeholder turn recorded.

* fix(core): fail closed on unknown quiet finish reasons

---------

Co-authored-by: yiliang114 <yiliang114@users.noreply.github.com>
2026-08-24 11:50:25 +00:00
Shaojin Wen
d1cfd87683
feat(review): promote language-pitfall and wrapper/proxy checks out of Agent 1a (#9805)
* feat(review): promote language-pitfall and wrapper/proxy checks out of Agent 1a (#9788)

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

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

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

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

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

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

---------

Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
2026-08-24 11:15:58 +00:00
顾盼
37cedea5b2
feat(computer-use): replace built-in tools with bundled skill (#9856) 2026-08-24 11:05:23 +00:00
易良
3892ca32ca
fix(core): map OpenAI-compatible finish_reason case-insensitively (#9884)
OpenAI-compatible gateways that front Gemini backends can return the
Gemini-native finish_reason spelling in uppercase (`STOP`, `MAX_TOKENS`).
The previous case-sensitive lookup mapped these to
FINISH_REASON_UNSPECIFIED, silently disabling MAX_TOKENS truncation
recovery.

Fold the reason to lowercase before lookup and add an explicit
`max_tokens` alias for the Gemini-native spelling.

Resolves #9882
2026-08-24 09:55:57 +00:00
Yu Zhang
6a21c436d6
test(core): align lazy generator vertex test (#9888)
Use an interface-supported method to exercise lazy Vertex initialization after the content generator interface was narrowed.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

Co-authored-by: zhangyu.34 <zhangyu.34@bytedance.com>
2026-08-24 09:24:26 +00:00
jinye
b5aec6691e
fix(serve): Canonicalize Live task bridge session IDs (#9819)
* fix(serve): Canonicalize Live task bridge session IDs

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

* test(serve): Pin canonical Live owner lookup

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

Follow-up to 18f08c0924: the test plan still required converter/tokenizer
focused tests for image paths; the image-tokenizer estimator cluster was
removed in PR #9676 (text tokenizer is unaffected and out of scope here).
2026-08-24 08:30:18 +00:00
易良
e0d933b23e
refactor(core): make derived Config ownership explicit (#8100)
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
SDK Python / Classify PR (push) Waiting to run
SDK Python / SDK Python (3.10) (push) Blocked by required conditions
SDK Python / SDK Python (3.11) (push) Blocked by required conditions
SDK Python / SDK Python (3.12) (push) Blocked by required conditions
Security Checks / Dependency CVE audit (push) Waiting to run
Security Checks / Secret scan (TruffleHog) (push) Waiting to run
* refactor(core): define derived config ownership

* docs(core): align derived config ownership scope
2026-08-24 07:48:49 +00:00
易良
27285a5243
refactor: centralize approval mode contracts (#9796)
* refactor: centralize approval mode contracts

* fix: align Python SDK import grouping

* test: restore approval mode exports in CLI mocks

* fix: close approval mode drift gaps

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

* fix: close approval mode review gaps

* test(cli): complete permission request fixture

* test(sdk): match approval mode route

* test(approval): close review coverage gaps

* test(sdk): cover approval mode global scope path
2026-08-24 07:46:50 +00:00
易良
a60cbbc54a
refactor(core): make utils/ a leaf layer (#9778)
* refactor(core): make utils/ a leaf layer

Eliminate every runtime (value) upward import from
packages/core/src/utils production modules so utils/ can become a leaf
layer with no runtime dependency on the rest of core.

Two mechanisms, no behavior change:

- Relocate domain-coupled modules out of utils/ into their owning
  module (agents, config, core, memory, services, tools), and move
  generic constants/types that live elsewhere into utils/. All
  `git mv` moves keep history; every import that pointed at a moved
  file is rewritten.

- Extract the remaining value imports as small leaf modules inside
  utils/ (AuthType, isTool, ToolErrorType, DEFAULT_QWEN_MODEL) and
  re-export them from their original owners so cross-package consumers
  are unaffected. doesToolInvocationMatch moves into shell-utils, its
  only production consumer.

Only type-only imports now cross the utils/ boundary. The two deferred
inversions in debugLogger (Storage, getTraceContext) are stateful and
left for a follow-up.

* chore(core): enforce utils/ leaf layer with lint rule

Add architecture/no-core-utils-upward-import, which flags runtime
(value) imports that leave packages/core/src/utils. Type-only imports,
sibling utils imports, and external package specifiers stay allowed;
the two deferred debugLogger inversions (config/storage,
telemetry/trace-context) are carried on an explicit allowlist.

Enable the rule as an error on core sources and cover it with
Linter-based tests.

* fix(core): restore iconv-lite tree-shaking for sync-file-encoding

The utils leaf-layer refactor moved sync-file-encoding from utils/ to services/, but the esbuild tree-shake plugin still matched the old ./utils/ specifier, so its sideEffects:false marker no longer applied and the ACP startup closure regained a static iconv-lite import. Point the onResolve filter at the new ./services/ path.

* fix(ci): catch stale integration imports earlier

* fix(core): close utils boundary review gaps

* fix(core): close self-reference boundary gaps

* ci: re-trigger after self-hosted runner checkout EACCES
2026-08-24 07:43:01 +00:00
易良
5b3830b46c
feat(vscode-ide-companion): adopt WebShell transcript as the default timeline (#9719)
* feat(vscode-ide-companion): reuse WebShell transcript UI behind experimental flag

Bridge ACP session/update notifications into the shared SDK daemon transcript reducer and render the result with the WebShell transcript component, gated on qwen-code.experimental.webShellTranscript (default off).

The WebShell renderer and its heavy transitive dependencies (echarts, mermaid, shiki, codemirror, katex) are lazily loaded via esbuild code splitting, so the default configuration keeps the ~700KB webview bundle unchanged.

* fix(vscode-ide-companion): grant wasm-unsafe-eval for shiki WASM when WebShell transcript enabled

* feat(vscode-ide-companion): adopt WebShell transcript as default timeline

Drop the experimental flag and the legacy MessageList renderer. The companion timeline now always renders through the shared WebShell transcript component, fed by ACP session/update notifications via the SDK daemon transcript reducer (lazy loaded through esbuild code splitting).

The flag-gated wiring is removed: the qwen-code.experimental.webShellTranscript setting, the conditional CSP/body attribute in WebViewContent, and the legacy MessageList path in App.tsx (~850 lines). The webview CSP now grants wasm-unsafe-eval unconditionally for Shiki's Oniguruma WASM.

* fix(vscode-ide-companion): reset WebShell transcript state on session switch

The experimental useAcpTranscript hook only consumed transcriptUpdate
messages, so its reducer state survived session boundaries. When the
extension switched sessions it kept the webview mounted and replayed the
newly-selected session through ACP, causing the previous session's blocks
to merge with the new replay (e.g. user text "alpha" from session A leaked
into session B as "alphabeta").

Reset both the reducer state and the rendered blocks on the same
boundaries the legacy message flow uses: qwenSessionSwitched (sent before
the ACP replay of the selected session) and conversationCleared (new
session). Adds a regression test that replays two sessions with a switch
between them.

* fix(vscode-ide-companion): harden WebShell transcript session boundaries

- reset the transcript on `conversationLoaded` too, closing the same
  cross-session leak the previous commit fixed for `qwenSessionSwitched`
  and `conversationCleared` (agent reconnect posts only this boundary)
- track the active session id and drop late `transcriptUpdate` frames
  whose `sessionId` no longer matches, so a previous session's trailing
  frames cannot contaminate the next session's timeline
- seed the transcript from cached messages carried by
  `qwenSessionSwitched` so offline restores and load-failure fallbacks
  render their history instead of a blank timeline
- dispatch `assistant.done` on `streamEnd`/`sessionLoadComplete` so the
  final assistant/thought block of a turn (or history replay) does not
  stay `streaming: true` forever

* fix(vscode-ide-companion): adopt live ACP session id after load-failure fallback

* fix(vscode-ide-companion): echo user prompt into WebShell transcript

* fix(vscode-ide-companion): keep WebShell transcript expanded and clear of the composer

* fix(vscode-ide-companion): surface local error and interrupt notices in the transcript area

* fix(vscode-ide-companion): restore file-link opening from the WebShell transcript

* fix(vscode-ide-companion): restore contributed copy commands for the WebShell transcript

* fix(vscode-ide-companion): add localOnly marker to TextMessage state type

* fix(vscode-ide-companion): restore /insight progress card and report link in the transcript UI

* fix(vscode-ide-companion): finalize in-flight tools on timeout and pin session-switch seeding guard

Map streamEnd reasons timeout/session_expired onto the reducer's error reason so abandoned mid-tool turns no longer spin forever (ceuI). Add qwenSessionSwitched cases with no messages field and an empty cache array; the no-messages case fails when the seeding guard is forced true, pinning its false side (ceuN).

* fix(vscode-ide-companion): remove unreachable editMessage backend and dead submit options

The user-message edit/rewind UI was dropped in the WebShell-transcript migration, leaving editTargetTurnIndex/onSubmitted options in useMessageSubmit and the full editMessage/rewind flow in SessionMessageHandler unreachable. Remove the dead options, the editMessage dispatch case, the rewind/snapshot flow with its recovery branches, and their tests (R1-8 direction b).

* fix(vscode-ide-companion): drop write-only loadingMessage bookkeeping

The waiting-message renderer was removed with the WebShell transcript migration and the user prompt is echoed into the timeline at send time (bd09e19d86), so the loadingMessage string was write-only dead state. Keep the isWaitingForResponse flag (submit gating / cancel) and pin its API surface (R1-19 direction b).

* fix(vscode-ide-companion): align waiting-flag pin test with the argument-less setter

* fix(vscode-ide-companion): echo attached images into the transcript timeline

The prompt carries pasted/attached images as ACP resource_link blocks,
which the transcript reducer cannot render (no inline data), so user
images vanished from the timeline while the attach path stayed alive.
Read each saved prompt image back from disk and echo it alongside the
text echo as an inline user_message_chunk image part (the daemon-echo
content shape), which the shared reducer folds into the user block and
the WebShell renderer already displays. Unreadable images are skipped
without breaking the send.

* fix(vscode-ide-companion): track live VS Code theme for the transcript

webShellTheme was snapshotted once at mount via useMemo with an empty
dependency array, so switching the VS Code color theme left the
timeline on the stale theme (VS Code updates data-vscode-theme-kind on
<body> in place without reloading the webview). Hold the theme in state
and refresh it with a MutationObserver on the body theme attributes.

* fix(vscode-ide-companion): copy every transcript block kind and map ambiguous row keys

- Copy All Messages now includes tool, shell, user_shell, and status
  blocks via getBlockCopyText, matching the pre-PR copyAllMessages
  handler which included formatted tool calls (review 5001842059 S-1).
- findBlockByRowKey prefers an exact id match and otherwise the longest
  matching block id, so one block id that dash-prefixes a sibling (e.g.
  `a` vs `a-1`) can no longer capture the sibling's row key (S-4).

* fix(vscode-ide-companion): drop whitespace-only cached transcript rows

cachedMessageToNotification rejected empty strings but admitted
whitespace-only content, which the reducer turns into an empty block
when seeding history from cached rows. Reject content that trims to
nothing (review 5001842059 S-2).

* fix(vscode-ide-companion): ship missing third-party notices in NOTICES.txt

Extend generate-notices.js so the regenerated NOTICES.txt carries the
attribution texts it previously only pointed at or dropped:

- Append license files from a package's licenses/ directory (echarts'
  Apache LICENSE references licenses/LICENSE-d3 for its embedded
  d3-derived files; the BSD-3-Clause text is now shipped).
- Append a package's NOTICE file when present (Apache-2.0 §4(d)),
  covering echarts' Apache Software Foundation attribution.
- Accept string-form package.json repository values (full URLs and
  GitHub shorthand) instead of emitting "(No repository found)".
- Fall back to the standard MIT text (copyright holder from package.json
  metadata) for MIT-declared packages that ship no license file.

* fix(vscode-ide-companion): show a recoverable error state when the transcript chunk fails to load

* test(vscode-ide-companion): gate the transcript blocks wiring into the WebShell renderer

* test(vscode-ide-companion): gate the transcriptUpdate forwarding from agent to webview

* fix(vscode-ide-companion): correct canonical MIT disclaimer wording in notices fallback

* fix(vscode-ide-companion): surface locally generated notices and aborted sends in the transcript UI

* fix(vscode-ide-companion): correlate streamEnd with the active request in the transcript hook

* fix(vscode-ide-companion): pin transcript session guard at clear/load boundaries with the fresh session id

* chore(ci): refresh cua workflow size baseline

* fix(vscode-ide-companion): split file links on raw # before percent-decoding

normalizeExplicitFileLink decoded the whole value before splitting on #, so an encoded %23 in a filename was treated as a fragment delimiter and truncated the path. Split on the raw # first and decode the path and fragment parts separately; the file:// branch decodes only the path component. resolveFileLinkFromAnchor also no longer runs URL decoding/fragment logic over the anchor-text fallback: the text is a literal path, which keeps the /export 'export (#1).html' links (whose file: href the sanitizer strips) clickable.

* fix(vscode-ide-companion): stamp cached history rows as discrete transcript messages

Cached-history seeding emitted each row as a bare *_message_chunk with no promptId/sourceRecordIds/_meta, so the shared reducer merged runs of consecutive same-role cached rows (Tool Result / telemetry / Plan rows per turn) into one plain-concatenated block, and a dropped whitespace-only user row let different turns fuse. Stamp every synthesized cached row with the reducer's existing anti-merge marker (_meta.qwenDiscreteMessage) so offline restores render the same discrete blocks as live replays.

* fix(vscode-ide-companion): resolve tool-group copy rows and copy tool content parts

Copy Message silently failed on every tool row: web-shell keys tool_group rows as msg:tg-<block id>, which findBlockByRowKey never matched. Strip the tg- prefix before the existing exact/longest-prefix matching; merged groups share the first block's key, so a group row resolves to the group's first tool block (documented). The tool case of getBlockCopyText also serialized only title + details (the input summary), dropping the output text and diffs the timeline renders; walk block.content and append text parts and ---/+++ diff renderings like the pre-PR formatToolCallForCopy did, restoring Copy Message / Copy All parity for tool rows.

* fix(vscode-ide-companion): close remaining transcript regressions

* fix(vscode-ide-companion): hide internal image references
2026-08-24 07:41:51 +00:00
易良
9ef32c5f3c
fix(core): make team shutdown a leader-only tool (#9401)
* fix(core): make team shutdown a leader-only tool

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

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

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

Fixes #9276

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

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

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

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

Three criticals from review, all real.

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

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

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

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

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

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

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

---------

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

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

* docs(serve): address ephemeral auth review

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

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

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

* docs(serve): centralize token selection plan

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

* docs(serve): make ephemeral auth opt in

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

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

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

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

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

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

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

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

* docs(serve): clarify temporary token storage

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

* docs(serve): clarify ephemeral token persistence

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

* codex: address PR review feedback (#9738)

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

* codex: address PR review feedback (#9738)

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-24 07:33:05 +00:00
Harjoth Khara
dbf7382c8f
fix(memory): scan uncapped when selecting forget candidates (#9530)
* fix(memory): scan uncapped when selecting forget candidates

Recall moved to the uncapped scanner in #8716; forget did not. A document
ranked past the 200-document cap could be recalled and injected into the
prompt but never forgotten.

Forget now scans uncapped, so its candidate universe matches recall's. The
model-selection prompt renders every candidate, so it gets its own bound of
400: literal query matches first, then the most recently modified remainder.
The heuristic fallback keeps scanning the full uncapped list.

Indexer, status, and extraction stay capped on purpose, and the two design
docs that recorded forget as capped now say otherwise.

Refs: #9378

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

* fix(memory): give each scope its own share of the forget prompt

Review round 1. The 400-candidate bound ranked both scopes into one recency
budget, so a store whose project entries are all newer than its user entries
seated no user memory at all. The capped scanners this replaced ran per scope,
so each scope always had seats. That made an old user entry unselectable by the
model while recall could still inject it, which is the same asymmetry the PR
set out to close.

Each scope now keeps a 200-candidate quota and whatever a smaller scope leaves
is handed to the other. Within a scope, literal query matches rank first and
both groups are ordered newest first, so truncation is deterministic instead of
scan-order, and the bound logs when it drops candidates.

Also from review: the query normalisation and match predicate are now shared
with selectByHeuristic so the two cannot drift; the user scan gets the
best-effort guard recall.ts and extractionAgentPlanner.ts already carry; and
the docstring and design docs no longer claim an unconditional guarantee the
bound does not provide.

Three tests, each verified against the mutation it is meant to catch: global
ranking drops the user ids, an ascending sort drops the newest filler, and
handing the fallback the bounded list returns 400 of 450 matches.

Refs: #9378

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

* fix(memory): bound the unconfirmed forget path and drop the silent scan guard

Review round 2, all suggestions.

MemoryManager.forget passed limit: MAX_SAFE_INTEGER and deletes without
confirmation. With an uncapped scan and a heuristic fallback that substring
matches the whole store, a one-character query matched nearly every entry in
both scopes, where the capped scanners had held that same failure to one scan's
worth of candidates. The limit is now the prompt bound, restoring the old
ceiling.

Round 1 added a best-effort catch on the user scan. That was wrong on two
counts: scan.ts caps after reading and ordering the whole tree, so uncapping
adds no read exposure to justify it, and swallowing the failure made forget
report "no entries matched" for a scope it never read, then act on that answer
by deleting. Reverted, with a comment saying why forget differs from recall
here: a missed injection is recoverable, a missed deletion is not.

normalizeForgetQuery now delegates to normalizeSummary so query matching and
the post-selection re-match cannot drift apart, and one design-doc sentence no
longer implies only semantic matches fall off the bound.

Two tests, each verified against its mutation: the quota split is now exercised
with both scopes over quota, where dropping it to 150 seats 250 project entries
instead of 200; and the delete ceiling fails at 401 removals if the unbounded
limit comes back.

Refs: #9378

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

* fix(memory): split forget's deletion seats per scope, and decouple the ceiling

Review round 3.

The deletion ceiling added last round truncated the heuristic fallback in
candidate order, and listIndexedForgetCandidates pushes every user entry ahead
of every project entry. With 450 matching user entries and 50 matching project
ones and the side query down, forget deleted 400 user entries, zero project
ones, and reported success. That is the reachability asymmetry this PR exists
to remove, moved into the delete path. The per-scope allocation the model
prompt already used is now shared with the heuristic, so each scope keeps its
share of the limit and a smaller scope's unused seats go to the other.

The ceiling is also its own constant now rather than an alias of the prompt
bound. Resizing the model prompt is a cost decision and resizing this is a
blast-radius decision; sharing one constant let the first silently widen the
second.

Two tests, each checked against its mutation: the 450-user/50-project shape
returns zero project matches under a plain slice, and oldest-first ranking
inside a scope drops that scope's newest entry from the prompt.

Refs: #9378

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

* test(memory): pin the forget split at a small limit and the heuristic's own order

Cross-review found both new tests mutation-survivable. Every case used a
400 limit, so hard-coding a 200 per-scope quota instead of deriving it from
the budget still passed, and the recency case let the side query succeed, so
it pinned the model prompt's ranking rather than selectByHeuristic's own
comparator.

One case at limit 5 with the side query failing covers both: it asserts the
3/2 split, which only holds if the quota comes from the budget, and that each
scope contributes its newest entry, which fails if the comparator is reversed.
Both mutants verified failing.

Refs: #9378

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

* refactor(memory): share the forget recency comparator and log a bound deletion

Review round 4, both suggestions.

The mtime comparator was the last thing the model path and the heuristic path
each typed for themselves, after this branch had already hoisted the query
normaliser, the match predicate and the per-scope allocator so the two could
not drift. Each site has its own test, so a one-sided ordering change would
have updated its own test, passed CI, and left the sibling stale. Now one
definition.

The deletion cap also bound silently. The prompt bound warns when it truncates;
the path that actually deletes did not, so a forget that removed 400 of 500
matches reported success and left no record of why recall kept injecting the
rest. It now says so.

No test for the new warning: it is a debug log line, and asserting on it would
pin the wording rather than the behaviour.

Refs: #9378

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-24 07:31:01 +00:00
qqqys
c7c4dc80e0
feat(core): add the output-style layer to the system prompt (#9565)
* feat(core): add the output-style layer to the system prompt

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

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

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

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

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

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

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

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

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

Two smaller corrections in the same area:

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

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

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

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

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

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

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

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

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

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

* fix(core): align output style prompt consumers

* test(core): pin Learning output style modes

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

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

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-24 07:19:57 +00:00
Stellar鱼
f241c19ace
fix(core): support per-provider stream idle timeout (#9795) 2026-08-24 06:46:25 +00:00
dreamWB
717ad101e2
feat(web-shell): add async submit preparation (#9802)
* feat(web-shell): add async submit preparation

* fix(web-shell): keep failed-prompt retries out of the daemon retry path

The failed-prompt retry reused `retry: true` only to skip prepareSubmit,
but the flag is also forwarded to the daemon, whose retry branch skips
recording the user message — the retried turn ran and was answered, yet
its user prompt never reached the transcript. Skip preparation with an
internal skipPrepareSubmit flag instead; the Ctrl+Y turn-error retry
keeps `retry: true`, where the daemon semantics are correct. Also
classify the retry payload from the prepared prompt so a host rewrite
that changes slash-ness still arms the turn-error retry.

Adds focused coverage for the resolvePreparedSubmit fallbacks,
preparation-time staleness cancellation on both paths, prepareSubmit
rejection, and the queued empty-prepared guard.

* fix(web-shell): disarm stale retry state after slash-prepared submits

* test(web-shell): pin slash-prepared disarm clears with attachment and stash variants

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

---------

Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-24 06:13:51 +00:00
ShiZai
4f53c7d160
fix(core): route debug logs through sessionIdContext before global session (#9538)
* fix(core): route debug logs through sessionIdContext before global session

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

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

Fixes #9535.

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

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

Relates to #9535.

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

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

Relates to #9535.

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

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

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

---------

Co-authored-by: tomsen-ai <230283659+tomsen-ai@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 05:51:24 +00:00
Yu Zhang
01416830c9
docs(vscode): correct minimum VS Code version (#9852)
Align the companion README requirement with the extension manifest so users do not try unsupported VS Code releases.

Co-authored-by: zhangyu.34 <zhangyu.34@bytedance.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-24 05:20:29 +00:00
Yu Zhang
4d3f9ff571
fix(core): allow hook-bounced workflow reapproval (#9547)
* fix(core): allow hook-bounced workflow reapproval

* fix(core): release settled workflow approval sources

* fix(core): reject stale workflow approvals

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

* fix(core): retry failed approval delivery

Record approval incarnations only after synchronous event delivery succeeds, preserving retry recovery while pinning scheduler bounce identity.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

* fix(core): log approval delivery failures

Expose failed approval event delivery while preserving retry-on-update behavior and consolidate the shared approval test setup.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

* fix(core): stabilize approval event delivery

Share responder state across retries, reject stale incarnations, and bound automatic redelivery so transient listener failures cannot stall or duplicate approvals.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

* fix(core): isolate approval delivery per listener

Retrying a partial approval delivery re-emitted the event to every
listener, so a single failing listener both blocked healthy ones from
ever seeing the request and re-delivered duplicates to listeners that
already succeeded (forwardApproval/createApprovalHandler have no callId
dedup). Push the isolation down to the emitter: iterate rawListeners,
catch per listener, and retry only the listeners that actually threw.

Surface retry exhaustion via console.error so it is visible outside
debug sessions, and drop the dead awaitingByCallId guard (a same-tick
snapshot compared against itself could never differ). Cover
thrower-before-healthy, healthy-before-thrower, exhaustion reporting,
and both clearApprovalDeliveries teardown paths (abort and
all-tool-calls-complete).

---------

Co-authored-by: zhangyu.34 <zhangyu.34@bytedance.com>
2026-08-24 05:01:22 +00:00
jinye
014b903bf5
fix(daemon): Bound conditional-close refusal holds (#9820)
* fix(daemon): Bound active-work close refusal holds

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

* codex: address PR review feedback (#9820)

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-24 05:00:40 +00:00
易良
3fd059368b
fix(core): invalidate token counts recorded for a switched model route (#9506)
* fix(core): invalidate token counts recorded for a switched model route

`/model` switches rebuild the content generator but keep the same
GeminiChat instance, so API-reported prompt/output token counts from the
previous route survived and anchored admission, output clamping, and
compression decisions for a different serialization (#9454).

Attribute the counts to the route that produced them
(Config.getModelRouteIdentity) and invalidate them on a route change so
all safety decisions fall back to the history-walk estimate.

Closes #9454

* fix(core): keep token counts scoped to producing route

* fix(core): close round-2 route-scoping findings (#9454)

- C1: client.test.ts session-token-limit gate test now seeds the chat
  mock's getLastPromptTokenCount (the gate's new source) instead of
  relying on the telemetry stub alone.
- C2: goal-turn-integration.test.ts passes the routeKey positional
  added to processStreamResponse and widens the local cast.
- S1: fix stale rationale comment — no session token-limit gate reads
  the telemetry mirror anymore; UI context counters and compression
  banners do.
- S2: pin tryCompress's entry invalidation with a focused test; manual
  /compress reaches it without sendMessageStream's entry reset.
- S3: direct config tests for getModelRouteIdentity — call stability,
  model@<sha-prefix> shape, and the guard keeping the registry baseUrl
  out of non-active model identities.
- R2-3: sendMessageStream now resolves the request route first and
  invalidates counts against IT (deriving the key from the actual model
  param on the non-exact branch), so an active-route count can no longer
  anchor an exact `\0` route's output clamp; regression test added.

* fix(core): close round-3 route-scoping findings (#9454)

- R3-1: hard-rescue rollback now restores tokenCountsRouteKey alongside the counts; tryCompress re-stamps the key to the active route mid-rescue, which left the resurrected request-route count riding the active key past the next entry invalidation (regression test: active-route read after failed rescue must not inherit the override count).

- R3-2(2): invalidation zeroes the telemetry cached-content mirror together with the prompt mirror so /context stops rendering a foreign cached count beside a zeroed prompt count; the mirror is documented as best-effort display state between a switch and the next guarded read (R3-2(1)).

- R3-4: collapse the requestRouteKey ternary into one getModelRouteIdentity call (the non-exact arm passed exactly the default parameter value).

- R3-5: resolve the active-route default lazily after the zero-count fast path instead of eagerly in the default parameter.

- R1-3: add the missing compressFast route-invalidation test (third entrypoint; mutation-verified).

* fix(core): preserve route-scoped token guards (#9454)

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

* fix(core): resolve full-turn route selectors at the session token limit gate (#9454)

* fix(core): stamp fallback-served token counts under the request route (#9454)

* fix(core): retain route-scoped token counts across route crossings (#9454)

* fix(core): keep route-scoped token counts consistent through compression (#9454)

* fix(core): keep rescued output counts and compression stamps route-scoped (#9454)

---------

Co-authored-by: yiliang114 <yiliang114@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-24 05:00:16 +00:00
C0d3N1nja97342
aac9606f78
fix(cli): skip terminal redraw optimizer on WSL/ConPTY (#7897)
* fix(cli): skip terminal redraw optimizer on WSL/ConPTY and enable sync output on Windows Terminal (#7634)

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

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

Fixes #7634

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

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

* review: address wenshao feedback on #7897

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

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

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

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

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

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

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

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

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

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

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

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

* docs(cli): document QWEN_CODE_LEGACY_ERASE_LINES escape hatch

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

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

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

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

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

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

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

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

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

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

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

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

Round-11 review: the comment asserted the optimizer is the ONLY path
emitting CSI 1 B / CSI n A, but the repo's patched ink build also emits
both sequence classes on its cursor-positioning path (buildCursorSuffix /
buildReturnToBottom, reachable via BaseTextInput.setCursorPosition).
Skipping the optimizer on WSL does not remove these from interactive
input. Narrow the claim to the per-frame erase-and-redraw path. #7897
2026-08-24 03:58:48 +00:00
callmeYe
a8b822f5d2
feat(web-shell): expose agent task changes (#9637)
* feat(web-shell): expose agent task changes

* fix(web-shell): deduplicate agent task callbacks

* fix(web-shell): ignore agent task telemetry churn

* fix(web-shell): skip immutable prompts in task fingerprint

* fix(web-shell): type agent task fingerprint
2026-08-24 03:54:49 +00:00
qqqys
5cff52c6e8
fix(goal): count catalog previews in the unit their budget is written in (#9835)
Evidence catalog previews were cut to 240 characters while the budget they
feed is 24,000 bytes. In UTF-8 those units differ by up to four times, so the
guard held only for ASCII. A legal 32-claim checkpoint of Chinese claims
serialized to roughly 29kB — over the cap on its own, before a single new
record had been scanned — which marked the window truncated, and `truncated`
switched compaction off. The one state compaction exists to resolve was the
one state it refused to run in, so the Goal was stopped as `usage_limited`,
the only status the reducer refuses to resume, with nothing left to salvage.
An English Goal never reached that state; a Chinese one could not avoid it.

Previews are now capped to 240 UTF-8 bytes on a code point boundary at the two
points a catalog entry is built. Nothing changes for ASCII, where the two units
already agreed; a CJK preview is shorter than before, which is the cost of the
cap actually holding. With it, a full checkpoint is bounded well inside the
catalog budget for every script, so a window can no longer start out truncated.

A truncated window now compresses rather than stopping the Goal. Overflow means
the budget is full and the newest evidence that did fit is exactly what a
checkpoint folds into claims; the older evidence left behind is already covered
by the previous checkpoint. Only a window that captured nothing at all has
nothing to salvage, and that is the sole remaining path to `usage_limited` here.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 03:27:52 +00:00
qqqys
674afdf1a7
feat(core): show what a workflow will do, and scope the grant that approves it (#9837)
A user approving a workflow was shown `Confirm WorkflowTool` over `Run a
workflow script (4127 chars)` — a character count standing in for arbitrary
model-authored JavaScript that can fan out to the per-run agent cap,
provision git worktrees and spend an uncapped token budget. The asymmetry
was visible inside one run: the subagent approvals that same workflow
bubbles up each got a full dialog.

Override getConfirmationDetails to show what the script says it will do,
read from `export const meta`: the workflow's name and description, its
declared phases, the resolved args, and a bounded excerpt of the source.
Meta is obtained through `extractAndStripMeta`, which parses rather than
evaluates, so nothing model-authored runs before the user has approved
anything. It throws on a malformed literal, so the call is wrapped: a
script with a broken meta block stays approvable-or-rejectable rather than
taking the dialog down with it.

Everything displayed goes through `stripAnsiAndControl` first. The screen
ships with the preview rather than after it, because until now nothing was
displayed and so nothing could be spoofed — a preview without it is what
would open the hole. Single-line fields are flattened, which is what we
want for a `meta.name` spanning three lines. The script excerpt is
sanitized per line instead: `\n` is a C0 control character, so the naive
call would collapse the script into one unreadable line.

Scope the grant on the same object. An inline `script` is fresh source
every time, so it can never be pre-approved: `hideAlwaysAllow` removes the
option, and an empty `permissionRules` stops `injectPermissionRulesIfMissing`
from supplying the bare tool name, which `buildPermissionRules` documents as
matching every invocation of the tool. A `scriptPath` names a file the user
chose, so it stays pre-approvable but scoped to that path via a `key:value`
param matcher. The rule is built with the same helpers the matcher uses, and
the test asserts it behaviourally -- a rule that reads plausibly but never
matches would make "always allow" silently do nothing.

Finally, move the token-cost warning ahead of the spend it warns about. It
previously appeared only on the success path, i.e. after the run, and never
at all when the run failed.


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

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 03:27:38 +00:00
Tianyuan
a2e458deef
feat(auth): add Kimi (Moonshot AI) as a built-in third-party provider (#9814)
Adds a Moonshot preset to the /auth Third-party Providers menu, offering
the international and China API endpoints and seeding the current Kimi
model catalog. Moonshot speaks the OpenAI protocol, so this is a
declarative preset with no new mechanism and no change to the provider
type.

Model metadata follows Moonshot's published capabilities. K3 is marked
thinking-mandatory: its API exposes a reasoning-effort knob but no way to
turn thinking off, so a disable shape must never reach the wire. The two
code models and K2.6 keep thinking toggleable, and all four accept image
and video input, which the K2.6 guide states explicitly.

Registers the new credential env key everywhere a provider key has to
appear: the no-AK CI gate and its pinned assertion list, and the
telemetry provider mapping, both by env key and by request hostname so
Kimi traffic is attributed rather than reported as unknown. The three
first-run docs that enumerate built-in providers are brought back into
agreement, which also picks up entries that were already stale.

Closes #9197
2026-08-24 03:18:40 +00:00
ytahdn
747dbf00c2
feat(web-shell): compact agent activity summaries (#9657)
* feat(web-shell): compact agent activity summaries

* fix(web-shell): preserve merged todo timing

* fix(web-shell): address compact summary review

* test(web-shell): cover compact review edge cases

* fix(web-shell): make the folded thought header row fully clickable

* fix(web-shell): guard folded thought toggle against popover clicks (#9657)

* test(web-shell): cover thinking header hit area and parallel-agent thoughts (#9657)

---------

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-24 02:46:07 +00:00
Shaojin Wen
03bcfe44b5
refactor(review): split SKILL.md into a core body plus verdict-gated reference files (#9804)
* refactor(review): split SKILL.md into core body plus verdict-gated references (#9787)

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
2026-08-24 02:41:49 +00:00
ShiZai
a7128d30c3
fix(core): isolate fork cache readers by session (#9471)
* fix(core): isolate fork cache readers by session

* fix(core): skip foreign cache extraction

* perf(core): skip foreign cache before memory IO

* test(core): provide session id in extraction fixture

---------

Co-authored-by: tomsen-ai <230283659+tomsen-ai@users.noreply.github.com>
2026-08-24 02:39:59 +00:00
Yu Zhang
deed96e733
fix(core): avoid ghost shutdown pending state (#9550)
* fix(core): avoid ghost shutdown pending state

* fix(core): gate assignments during shutdown delivery

Keep teammates out of task assignment while shutdown delivery is pending, with rollback on mailbox failure.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

* fix(core): preserve concurrent shutdown state

Track in-flight and delivered shutdown requests independently so a failed retry cannot reopen assignment gates owned by another request.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

* fix(core): settle resolved shutdown writes

Keep response resolution separate from mailbox delivery so late writes cannot re-arm a completed shutdown generation.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

* fix(core): separate shutdown marker ownership

Track delivered requests independently from test-only markers so failed writes clean up without dropping pending successful deliveries.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

* fix(core): track shutdown response ownership

Represent delivered shutdown requests as reservable tokens so concurrent writes and responses conserve pending state without stale generations or duplicate aborts.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

* fix(core): deliver every stacked shutdown request

A busy teammate that received two shutdown requests only ever saw the
first: one idle flush read all unread shutdown messages but enqueued
only shutdowns[0], leaving the rest marked-read and dropped. Each write
still minted a delivered token, so the un-delivered request's token
could never be consumed by a response and hasShutdownWork stayed true
forever, permanently gating task assignment for a live teammate.

Enqueue every shutdown returned by a single flush so token count and
delivered-request count stay balanced. Add coverage for stacked
delivery, markShutdownRequested idempotence, the responsesInFlight
gating window, and abort-on-approve when a MESSAGE_SENT listener throws.
Extract the repeated gated-mailbox-write scaffolding into one helper and
reset the shared mailbox mocks per test.

* chore(core): log shutdown ledger state transitions

Add debug logging at the shutdown state-machine transitions (token
mint, response settlement, ledger drain) so a stuck shutdown-pending
teammate can be diagnosed from logs without reading private fields.

Addresses reviewer suggestion on #9550.

---------

Co-authored-by: zhangyu.34 <zhangyu.34@bytedance.com>
2026-08-24 02:36:23 +00:00
qqqys
75fb40d832
refactor(goal): render Goal continuation prompts from one core renderer (#9581)
* refactor(goal): render Goal continuation prompts from one core renderer

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

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

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

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

* fix(goal): tighten continuation renderer contract

* test(goal): cover verifier feedback hosts

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-24 02:36:02 +00:00
易良
00461ab275
fix(core): mark agent launch failures as failed tool calls (#9519)
* fix(core): mark agent launch failures as failed tool calls

The subagent-not-found path and failWorktreeProvisioning() returned a
ToolResult with llmContent and a failed returnDisplay but no error
field. The scheduler records a failure only when error is set
(coreToolScheduler keys off toolResult.error), so a launch that never
ran was counted as a successful agent call: no error-formatted model
response, no failure-path hooks, and optimistic success telemetry.

Set error consistently with buildSpawnBlockedResult in the same file,
which already documents this convention. The full guidance text (the
available-subagents list, the specific provisioning reason) goes into
error.message because the failure path forwards only that to the
model. returnDisplay is unchanged, so terminal output is identical.

Fixes #9509

* fix(core): mark agent launch failures

* test(core): cover preserved agent worktree errors

* test(core): keep preserved worktree error test foreground

* test(core): fail subagent via creation seam in preserved-worktree test
2026-08-24 02:34:49 +00:00
Shaojin Wen
20e076f8f3
fix(core): surface nested sub-agent approvals under background parents (#9793)
* fix(core): surface nested sub-agent approvals under background parents

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

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

Fixes #9782

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-24 02:30:09 +00:00
tao943
3a9d2d37f8
docs(agent): clarify parameter preconditions (#9580)
* docs(agent): clarify parameter preconditions

* test(agent): cover read-only preconditions

* docs(agent): clarify nested background downgrade

* docs(agent): align working-dir background guidance

* docs(agent): clarify teammate worktree execution

---------

Co-authored-by: tao943 <278275162+tao943@users.noreply.github.com>
2026-08-24 02:27:09 +00:00
易良
5cb946b55b
feat(scheduled-tasks): allow creating a task with an existing session (#9361)
* feat(scheduled-tasks): allow creating a task with an existing session

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

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

Closes #8906

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix(core): preserve subagent continuation retries

* test(core): cover plain subagent retry forwarding

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

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

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

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

Fixes #8877

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

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

Also from review: delegate setupRecorder to setupRecorderWith in the
InputPrompt tests, cover the prompt->denied status transition (re-warns
as an error), and assert Composer forwards the session ref with stable
identity across input-active toggles.
2026-08-24 02:15:04 +00:00