Skip to content

[pull] main from QwenLM:main - #534

Merged
pull[bot] merged 20 commits into
bit-cook:mainfrom
QwenLM:main
Sep 1, 2026
Merged

[pull] main from QwenLM:main#534
pull[bot] merged 20 commits into
bit-cook:mainfrom
QwenLM:main

Conversation

@pull

@pull pull Bot commented Sep 1, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

yiliang114 and others added 20 commits August 31, 2026 23:37
* fix(core): show edit/exec diffs when a PreToolUse hook returns ask

When a PreToolUse hook returns an 'ask' decision, the tool was bounced
back to awaiting_approval with a synthetic plain-text prompt carrying
only the hook reason, so Edit/WriteFile confirmations lost their diff
view. Reuse the tool's own confirmation view (edit diff / exec command)
when it provides one, attaching the hook reason via a new hookAskReason
field rendered above the body. Tools without a structured view keep the
plain reason prompt.

Fixes #9434

* fix(core): preserve hookAskReason when restricting bubbled edit/exec confirmations

* fix(core,cli): close PreToolUse ask-bounce abort/rendering gaps (#9434 follow-up)

* test(core): pin hookAskReason passthrough in the approval restriction

* fix(shell): keep a user-confirmed sed edit across a re-entrant preview

The PreToolUse ask bounce re-fetches getConfirmationDetails, which
re-ran prepareSedEdit and unconditionally cleared confirmedSedNewContent,
discarding the content the user already edited and approved. Only clear
it when the freshly prepared edit differs from the previously prepared
one (a genuinely new sed target).

* fix(core): re-apply plan-shell policy in the PreToolUse ask bounce

The bounce built its confirmation from the tool RAW details, so
hideModify/skipIdeDiff/warnings and validatePlanModeShellApproval were
dropped. Decorate the bounced details with the plan-shell decision and
wrap onConfirm to validate at approval time, mirroring the ordinary
confirmation phase.

* fix(core): close round-4 ask-bounce findings (#9434)

Carry the confirmation-phase plan-shell decision into the execution
phase via a per-callId map so the PreToolUse ask bounce compiles and
re-applies decoratePlanModeShellConfirmation / validatePlanModeShell
Approval (R4-1, R3-2). Re-enter the captured invocation context around
the bounced onConfirm so the tool-invocation guard sees the
invocation's identity, not the responder's (R4-4). Collapse the two
inline cancel-before-execution copies into cancelWithSyntheticResponse
(R3-5) and correct the bounce docstring (R3-4).

shell: keep a prepared/confirmed sed edit authoritative when a
re-entrant preview fails instead of flipping to raw execution (R4-3),
and present the retained confirmed content in the re-confirmation view
so it matches what approval writes (R4-2).

cli: render hookAskReason on the stream-json permission suggestions
and the ACP permission request content (R3-3).

Tests: hook-reason cap on short terminals (R3-7), exec-branch
hookAskReason passthrough (R3-8), abort-resolves and reason-fallback
bounce variants (R3-9), sed retention across a re-entrant preview
(R4-5), structured-bounce decline (R4-6), plan-shell bounce through
the scheduler (R4-7).

* fix(core): close round-5 ask-bounce findings (#9434)

* fix(core): close round-6 ask-bounce findings (#9434)

R6-1: a bounced re-execution skips the PreToolUse hook, so the
confirmation surfaces while bounced must not change what runs:
- the IDE diff is no longer opened on a bounce; it answered through
  the pre-wrap details, bypassing dropBounceModifyPayload
- ModifyWithEditor is rejected while bounced (the call stays in
  awaiting_approval; Cancel/Proceed still recover)
- the hook-reviewed request.args snapshot is restored before the
  bounced re-execution, closing the stream-json responder channel
  that mutates request.args directly before answering

R6-2: the bounced plan-shell branches now use the ordinary
confirmation phase's planShellResponseClaimed guard (checked and set
synchronously before the validation await), so racing answers to the
same bounced call collapse to one handleConfirmationResponse.

* fix(core): invalidate round-1 IDE diff on PreToolUse ask bounce (#9434)

A round-1 openDiff resolver that survives into a bounced round (sibling
auto-approval never closes the diff) could answer the bounced confirmation
through the status-only awaiting_approval guard, bypassing the bounced
onConfirm protections (hideModify, dropBounceModifyPayload, claim guard).

Bounce now bumps a per-call confirmation epoch and resolves/closes the
outstanding round-1 IDE diff the same way the CLI's handleConfirm does;
openIdeDiffIfEnabled drops its answer when the epoch has moved on. Adds
accept/reject arm regression tests. (#9434 review R7-1)

* fix(core): close round-8 ask-bounce findings (#9434)

- R8-1: pass hookAskReason through the info branch of
  restrictWorkflowConfirmationDetails, mirroring edit/exec, so bubbled
  info approvals keep the hook reason in the leader UI (+ sentinel test).
- R8-2: the ask bounce no longer awaits resolveDiffFromCli on the
  critical path (closeDiff is bounded only by the 10-minute IDE RPC
  timeout and takes no signal); the invalidation is fire-and-forget and
  an abort re-check precedes the status transition. Epoch invalidation
  still drops stale round-1 answers (+ regression test with a hung RPC).

* test(core): remove duplicate scheduler stub

* test(cli): colocate permission-suggestions test with its module

The test lived in src/utils/ and imported ./permission-suggestions.js,
but the implementation is src/nonInteractive/permission-suggestions.ts,
so vitest collection failed with TS2307 and the tests never ran.
Move the test next to the module per house convention; no test-body
changes needed.

* fix(core): harden PreToolUse ask bounce confirmations

- Forward the confirmation payload's cancelMessage through the bounced
  edit onConfirm wrapper (the info fallback and the pre-PR synthetic
  prompt both forwarded it), so a stream-json host denial keeps its
  reason instead of falling back to 'User did not allow tool call'.
  newContent stays excluded: bounced confirmations set hideModify, so
  the modify channel must not rewrite hook-reviewed content.
- Refuse round-1 IDE diff resolutions for calls in bouncedAwaitingApproval:
  a bounce re-enters awaiting_approval, which let a stale openDiff
  resolution answer the bounced confirmation — the accept path flowed
  panel content through _applyInlineModify (type 'edit', hideModify not
  checked there) and executed it on the hook-skipping re-execution.

Adds regression tests for both; the IDE test goes red when the guard is
removed and the cancel-reason test goes red when forwarding is dropped.

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(java): read control response subtype from payload

Use the parsed control response wrapper when deciding whether a CLI control response is an error, matching the nested wire format and preserving the legacy top-level fallback.

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

* fix(java): keep sendPrompt stream aligned after control errors

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

* test(java): align control error fixture

Use the real control-response error envelope in session tests so the subtype handling coverage matches the CLI wire format.

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

* fix(java): simplify control response subtype lookup

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

* test(java): cover control response warning logs

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

* test(java): strengthen control response subtype coverage

Cover missing response payloads, non-error subtype boundaries, and warning payload content so subtype detection regressions are observable.

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

* test(java): pin control response subtype boundaries

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

---------

Co-authored-by: zhangyu.34 <zhangyu.34@bytedance.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
… compaction (#10408)

* fix(core): recover model requests rejected with HTTP 413 via one-shot compaction

When an OpenAI-compatible endpoint sits behind a reverse proxy with a
request-body byte limit, a session below the token-based auto-compaction
threshold can still have its serialized request rejected with a bare
HTTP 413 (often an HTML error page). The token-wording overflow detector
never matched it, reactive compression never fired, and every subsequent
prompt re-sent the same oversized history, leaving the session
permanently broken (#10380).

- Add a model-request-scoped 413 detector (utils/request-payload-error.ts)
  consulted only by llm-chat's send catch, so upload/file 413s are never
  misclassified as context overflow.
- Route a payload-overflow 413 into the existing one-shot reactive
  compression path and retry once.
- Truncate oversized tool-result/text payloads in the compaction
  side-query input on this path so the side-query itself fits under the
  same gateway byte limit.
- Surface an actionable "start a new session" error when recovery cannot
  shrink the request under the limit, preserving the HTTP status.

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

* fix(core): suppress post-compact restoration on payload-overflow compaction

Restoration re-embeds full-size image payloads and file blocks that the
slimmed 413 side-query never carried, re-inflating the rebuilt retry
request back over the gateway byte limit. On the requestPayloadTooLarge
path compose post-compact history with maxFiles/maxImages=0 so the retry
is dominated by the summary that just fit.

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

* fix(core): keep part siblings and slim functionCall args in 413 slimming

Truncated text parts were rebuilt as bare { text }, dropping sibling
properties such as thought/thoughtSignature that the converter pipeline
keys reasoning content off. Spread the part instead. Also truncate
oversized top-level functionCall string args (write_file/edit carry
whole file contents there) on the payload-overflow path, matching what
estimatePartChars already bills.

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

* fix(core): keep cause-wrapped 413 status on the actionable error

Detection walks the .cause chain five levels, but the status copy onto
the surfaced actionable error used the shallow top-level lookup, so a
cause-wrapped 413 lost its .status and downstream bucketing recorded
unknown. Expose the deep-found status on RequestPayloadTooLargeInfo and
reuse it when copying.

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

* fix(core): give 413 recovery outcome-accurate advice instead of unconditional /clear

The one-shot payload-overflow wrap fired the same "start a new session
(/clear)" error for every non-COMPRESSED outcome, including two outcomes
where that advice is false (#10380):

- Transient compaction failure: a side-query 504/reset made compress()
  throw, the catch swallowed it, and the wrap advised /clear — but
  reactiveCompressionAttempted is per-send, so the next prompt gets a
  fresh one-shot and may recover. Keep the original 413 instead so the
  next send can retry recovery.
- NOOP compaction: no earlier history to compress means the oversize
  sits in the current request itself; /clear + retry reproduces the
  identical failure. Surface a "reduce the current request" variant
  instead.

Only the genuine "compaction ran and still did not fit" outcome keeps
the new-session advice. Adds the NOOP message constant and two
regression tests that fail when the outcome split is reverted.

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

* fix(core,cli): report payload_overflow trigger reason for 413-driven compactions

A successful 413-driven compaction reported triggerReason 'token_limit'
(the reactive path passes trigger 'auto' and only 'image_overflow' was
ever upgraded), so the CLI notice claimed the conversation "approached
the input token limit" and the recorded payload logged a fabricated
window-sized token count — contradicting the very premise that 413 fires
below the token threshold (#10380).

Add a 'payload_overflow' member to CompactionTriggerReason, set it in
compress() when opts.requestPayloadTooLarge, and give the CLI notice a
matching clause ("exceeded the endpoint request-body limit"). The
reactive 413 path is force=true, so the screenshot-trigger upgrade in
the non-forced gate cannot overwrite the reason. Covered by a service
test asserting the COMPRESSED result's triggerReason and a hook test
asserting the notice wording; both fail when the fix is reverted.

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

* fix(core): label and log the 413 recovery path accurately for oncall

The new recovery path was silent or mislabeled exactly where an
incident needs it (#10380):

- No log at the decisive wrap transition: add a warn when recovery is
  exhausted and the actionable 413 error is surfaced.
- The two adjacent warns still said "context overflow" on the HTTP 413
  payload-overflow path; split them on requestPayloadOverflow.isTooLarge
  the way the first branch already does.
- stats.textPartsTruncated was written but never logged, and the
  runColdCompression slimming log was gated on images/documents being
  non-zero, so text-only slimming (the typical 413 case) logged nothing;
  include it in both the condition and the message.

Logging-only change; no behavior impact.

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

* fix(core): anchor 413 recovery accounting on a local history estimate

A bare HTTP 413 carries no provider token counts, so the reactive
originalTokenCount fell through to the full context window. compress()
then anchored its newTokenCount math on that window and stamped the
post-compaction count at roughly window minus visible history — orders
of magnitude above the real size. The inflated count persists into the
next turn, force-re-compacting the just-compacted history or
false-tripping a configured sessionTokenLimit right after a successful
recovery (#10380).

On the payload-overflow route, anchor on estimateContentTokens over the
actual history — the same estimator the missing-usage accounting path
already uses — instead of the window. Provider-reported counts
(actualTokens / limitTokens) still take precedence, and the
token-wording overflow path keeps its existing projection.

Witness: new llm-chat test asserts the reactive compress() call carries
an estimate far below the window; it fails (200000 >= 10000) when the
anchor reverts to the window fallback.

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

* fix(core): keep 413 recoveries off the cache-sharing side-query path

The cache-sharing request is built from the UNSLIMMED history, but a
payload-overflow compaction exists because that exact payload was just
rejected at the gateway byte limit. On mixed errors (413 status plus
provider-reported counts that fit the window) canShareCache flips true
and re-uploads the rejected payload at full size, takes another 413,
logs a misleading cache-sharing failure, and only then recovers via the
slimmed cold path — a wasted round-trip and a hole in the byte-limit
invariant the slimming exists to enforce. It widens further now that
the 413 anchor is a truthful estimate (sharedRequestFits flips true
for every 413 compaction).

Add !opts.requestPayloadTooLarge to canShareCache so payload-overflow
recoveries use the slimmed cold path exclusively (#10380).

Witness: cache-sharing fixture where every other conjunct holds;
compress() with requestPayloadTooLarge must never call generateText.
Removing the conjunct makes the cache-sharing call reappear (red).

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

* test(core): pin the maxFiles half of payload-overflow restoration suppression

The suppression test added for #10380 builds its history exclusively
from screenshot payloads, so only the maxImages: 0 wiring can turn it
red; mutating maxFiles back to tuning.maxRecentFiles kept every PR test
green. Large sessions are dominated by read_file blocks (~20KB each),
so a regression on the file half re-inflates the rebuilt retry request
past the same gateway byte limit — a second 413 after recovery was
already achievable.

Add a sibling test with a real read_file result on disk: on the
token-driven path the file content IS re-embedded (live comparator),
and with requestPayloadTooLarge the serialized post-compact history
carries no file-restoration block. Goes red when maxFiles reverts to
tuning.maxRecentFiles.

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

* test(core): cast the restoration-suppression fake config to Config

Spreading the mocked Config loses its member types, so the baseOpts
config no longer satisfied CompressOptions after the getTargetDir
override. Restore the Config type with an assertion.

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

* fix(core): handle payload-overflow compaction edge cases

Treat returned compression API failures like transient side-query failures so a recoverable 413 does not receive permanent new-session advice. Keep payload-overflow token accounting off the slimmed side-query usage branch, and avoid splitting surrogate pairs when trimming request text for compaction.

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

* test(core): make 413 compaction fixtures carry a realistically large visible history

The payload-overflow tests mocked a 90K/180K-token payload over a
~14-token visible history, so the estimate-based accounting (which
413 recoveries now use) correctly flagged the summary as an inflation.
Give the fixtures large visible content, matching the real shape of a
gateway-rejected payload, so they verify trigger reason and cold-path
routing against a genuine reduction.

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(core): make loop detection result-aware for task_list polls (#9450)

Identical task_list arguments do not imply an identical result: teammates
mutate the shared task board between calls. The argument-only loop guards
falsely halted polling teammates with 'duplicate tool-call loop detected'
while the board kept changing.

Record executed tool results into LoopDetectionService (agent runtime after
processFunctionCalls; main-session continuations via functionResponse callId
pairing) as privacy-safe SHA-256 fingerprints. For the stateful read tool
(task_list only), the consecutive-identical guard, the global-duplicate
heuristic, the adaptive cap stuck signal, and action stagnation now require
the observed results to be unchanged too. Missing result evidence fails safe
and keeps the pre-fix behavior, preserving the DashScope #5019 protection.

Loop stops become attributable: ReasoningLoopResult carries the exact
LoopType, the interactive stop message and the headless FINISH event /
telemetry completion record include it.

* fix(core): report result-aware loops to arena

* fix(core): complete loop attribution plumbing for #9450

- R1-1: declare loop_type on SubagentExecutionEvent and serialize it in
  the QwenLogger sink; previously the spread in agent-headless.ts was
  dropped at construction (excess-property spread), so LOOP_DETECTED
  stops reached the journal unattributable.
- R1-2: reset loopType at the top of AgentHeadless.execute() so a
  re-executed instance (stop-hook continuation, resident turns) never
  carries a stale loop attribution into an ERROR/FINISH record.
- R1-12: pair only stateful read tools in requestByCallId — every other
  tool is rejected by recordToolResult anyway, and the write-only entries
  pin full args objects (write_file contents) up to the eviction cap.
- R1-3: drop the dead 'key' field from requestByCallId entries; the
  consumer reads only name/args and recordToolResult recomputes the key.
- Tests: assert loop_type in the telemetry completion record (R1-5) and
  pin the re-execution attribution reset.

* test(core): pin result-aware loop guards against silent regressions (#9450)

- Extend the changed-results global-duplicate phase past GLOBAL_DUPLICATE_THRESHOLD rounds so an args-only mutant cannot hide.
- Add heuristic-gate tests that Retry and reset() clear result-aware pair counts (cross-prompt and replay false positives).
- Add the adaptive-cap positive-side test: an interleaved frozen poller halts with TURN_TOOL_CALL_CAP just past the soft cap.
- Add the streak-move fail-safe test: a resumed streak with insufficient fresh result evidence halts instead of trusting stale evidence.
- client.test.ts: drive sendMessageStream with paired ToolResult ids — frozen board halts with global_tool_call_duplicate, changing board keeps running.
- agent-headless: interleaved frozen task_list polling halts via the result-time guard with FINISH loopType attribution.
- agent-interactive: LOOP_DETECTED results surface the exact detector in the stop message and lastRoundError.

* test(core): pin loop_type journal mapping and stateful callId guard (#9450)

* fix(core): keep result-aware loop guards alive for persisted oversized results (#9450)

Oversized results are rewritten into persistence stubs whose envelope embeds
a per-call unique file path (<toolResultsDir>/<callId>.txt), so fingerprinting
the whole model-visible response made every fingerprint unique and silently
disabled every result-aware guard for exactly the largest results: a frozen
task_list board over the persistence gate was never halted. Fingerprint the
semantic payload instead — strip the stub envelope and hash the
preview/truncated content that follows the stable marker.

* refactor(core): keep loop-guard stub parsing in lockstep with the stub builder (#9450)

* fix(core): fingerprint the full output in persistence stubs (#9450)

* test(core): cover unwrapped and truncated oversized stub shapes (#9450)

* fix(core): fingerprint the full output in truncated stubs (#9450)

truncateAndSaveToFile's stub shape carried no full-output digest (unlike
buildStub), so a shared board mutating in the truncated middle band
fingerprinted identically on every poll and the result-aware guards
halted a productive poller. Embed the sha256 of the full pre-truncation
output in both the wrapped and the unsaved (disk-failure) shapes, and
make the loop guards' stub parser prefer the digest in every shape
before falling back to the visible payload.

* fix(core): fingerprint batch-budget fits for the loop guards (#9450)

The batch-budget finalizer's fitText header embeds a per-call artifact
path, so every oversized batch-budget result fingerprinted uniquely and
the result-aware loop guards were silently disabled for exactly those
results. Embed the sha256 of the full pre-fit text in the header (right
after the constant prefix so tiny allocations that slice the header
still keep it); the guards' digest-first stub parsing picks it up.

* fix(core): make stateful pair counts order-aware (#9450)

The result-aware (repeat key, result fingerprint) counter accumulated
turn-wide, so a board oscillating between two byte-identical states
reached GLOBAL_DUPLICATE_THRESHOLD on one state and halted a poller
whose every result differs from its predecessor — contradicting the
recorded invariant that a changed result is observable progress. Count
consecutive identical results per key instead: interleaved frozen polls
still accumulate, but any result differing from the key's predecessor
restarts the count.

* fix(core): make the alternating-pattern guard result-aware (#9450)

checkAlternatingPattern still counted task_list args-only at request
time, so a poller alternating task_list with another call (the ABAB
shape of check-board-do-work) was halted on the first full window even
while the board kept changing. Apply the same result-aware carve-out
the sibling detectors got: a stateful participant's rolling results are
checked when the window fills, any changed result restarts the window,
and missing result evidence fails safe into the argument-only halt.

* fix(core): disarm the cap on thawed boards and anchor stub digests (#9450)

Two result-aware guard hardenings from review:

1. The adaptive cap's stateful stuck signal no longer latches a high-water
   peak. recordToolResult now feeds the cap a statefulCapKeyRepeat that
   falls back to the keys' current streaks when a result changes, so a
   frozen-then-thawed task board releases the cap exactly as it releases
   the result-time global-duplicate count. A permanently frozen board still
   arms the cap and halts just past the soft cap.

2. stripPersistenceEnvelope no longer honors the FULL_OUTPUT_DIGEST_LABEL
   marker anywhere in a string. Stub recognition is gated on the producer
   prefixes (persisted-output / output-too-large / truncated / batch-budget
   fit) and the digest must be line-anchored with a full 64-hex payload, so
   board content that merely quotes a stub's digest line is fingerprinted
   verbatim instead of collapsing to (or varying with) the quoted window.
   The batch-budget fit prefix is now an exported constant
   (BATCH_BUDGET_FIT_PREFIX) so the parser shares the producer's literal.

extractToolResultText / fingerprintToolResult / isStatefulReadTool are
exported so the daemon turn-loop guard fingerprints results identically
(issue #9450 requirement #6).

* fix(core): count deduped provider call ids once in the subagent loop guard (#9450)

Request counts and result evidence were fed from different populations: the
consecutive-identical request counter counted every streamed function call
(pre-dedup), while results land once per deduped executed call. A
provider-emitted duplicate call id — the exact pathology dedupeToolCallsById
exists for — left the request counter one ahead of the result evidence, so
the result-aware exemption failed safe and halted a fully productive
task_list poller.

The subagent stream loop now feeds the loop guard one event per call id per
attempt (a per-round Set mirroring dedupeToolCallsById; id-less calls are
never deduped), keeping request counts and result evidence on the same
population. Cleared on retry alongside the accumulated function calls.

* fix(core): surface subagent loop attribution in the FINISH display (#9450)

AgentFinishEvent.loopType was write-only: agent-headless emits it on every
loop-halted run, but no production code read it, so the FINISH surface
collapsed every loop stop into the generic LOOP_DETECTED label. AgentTool's
FINISH handler now reads loopType and appends it to the failed task card's
terminateReason (the same `(loopType)` attribution agent-interactive already
renders); the journaled sink remains SubagentExecutionEvent.loop_type.

* fix(cli): make the daemon loop guard result-aware for task_list polls (#9450)

The daemon/ACP mirror (recordDaemonToolCalls) still counted task_list
args-only at request time, so the #9450 false positive survived on that
runtime: a daemon-served leader polling task_list with identical arguments
while peers kept completing tasks hit the global-duplicate mirror at the 6th
request (skipLoopDetection=false) or the adaptive cap's stuck signal past
the soft cap, even though every executed result differed. Issue #9450
requirement #6 asks main-session, subagent, and ACP paths to behave
equivalently where they support the same tool.

recordDaemonToolCalls now skips stateful read tools at request time, and a
new recordDaemonToolResult records each executed result post-execution
(wired where executed results are queued), keyed on (call, result
fingerprint) via the shared fingerprintToolResult. It feeds the cap's stuck
signal (statefulMaxResultRepeat, disarmed on a changed result, mirroring
core's statefulCapKeyRepeat) and a result-time global-duplicate count
(gated on skipLoopDetection, mirroring core's recordToolResult). Batch loops
and the final finalize observe loopState.loopDetected so a result-time
detection stops the turn.

* fix(core): count deduped provider call ids once in the main-session loop guard (#9450)

Main-session twin of the subagent fix (1ac2a81): request counts and
result evidence were fed from different populations. Turn yields one
ToolCallRequest event per raw streamed function call (no dedup), so a
provider-emitted duplicate call id advanced the consecutive-identical
counter twice, while execution collapses duplicates (scheduler
dedupeRequestsByCallId / interactive duplicate-call-id suppression) and
recordToolResultByCallId consumes once per call id — leaving the request
counter permanently one ahead of the result evidence within a streak.
The result-aware exemption then failed safe and halted a productive
task_list poller whose board changed on every poll.

The main-session stream loop now feeds the loop guards (always-on and
heuristic tiers) one event per call id per attempt — a per-attempt Set
cleared on retry/model-fallback alongside the attempt's accumulated
state, mirroring dedupeRequestsByCallId (id-less requests are never
deduped). The events themselves still flow to stream consumers; only the
guard feed is deduped.

* fix(core): carry the inner stub digest through batch-budget fitting (#9450)

* fix(core): decay abandoned stateful streaks at round-trip boundaries (#9450)

* fix(cli): decay abandoned stateful streaks at daemon batch boundaries (#9450)

* fix(core): make the alternating-pattern carve-out in-flight aware (#9450)

* fix(core): keep degenerate batch-budget fits content-dependent (#9450)

* fix(core): exclude never-executed synthetic results from the subagent loop guard (#9450)

* fix(core): exclude synthetic duplicate responses from the main-session loop guard (#9450)

* fix(cli): skip daemon abandonment decay for batches that execute nothing (#9450)

* fix(core): canonicalize loop-guard fingerprints across the batch-budget fit (#9450)

* fix(core): unwind suppressed replays and skip decay for requested keys (#9450)

* fix(cli): skip daemon batch decay for re-requested stateful keys (#9450)

* fix(core): keep sub-label batch-budget fits content-dependent (#9450)

* test(core): align the replay-in-streak halt with the suppression unwind (#9450)

* fix(core): make the always-on consecutive guard in-flight-aware for parallel stateful batches (#9450)

The exoneration gate assumed the prior N-1 results of the Nth identical
request had all been recorded, but with parallel tool batches ALL of a
round's identical task_list requests stream through the guard before ANY
of that round's results lands (dedupeToolCallsById collapses only
same-callId duplicates). The gate became unsatisfiable and a productive
changing-board poller halted CONSECUTIVE_IDENTICAL_TOOL_CALLS — the #9450
false positive re-entering via a parallel batch.

Give the always-on guard the same per-key in-flight accounting the
alternating-pattern carve-out already uses: maintain statefulInFlight in
the always-on path (checkAlwaysOnSafeties) so it also works under the
skipLoopDetection default, and judge the gate on
toolCallRepetitionCount - inFlight results (floored at the recorded
evidence). A genuine wiring gap (shortfall with no recorded evidence)
still fails safe and halts, preserving the #5019 protection.

* fix(core): reset the consecutive streak when its result evidence decays (#9450)

The Finished-boundary decay zeroed a key's result evidence
(resultsObserved / unchangedStreak) after two consecutive mark-less
round-trips, but the always-on consecutive streak survived untouched.
When polling resumed mid-streak, the exoneration gate could never be
satisfied again (resultsObserved can only ever reach count - 2), so a
changing-board poller halted CONSECUTIVE_IDENTICAL_TOOL_CALLS — the
#9450 false positive re-entering via the decay layer.

Drop the consecutive streak together with its evidence in
decayAbandonedStatefulStreaks so resumed polling starts a fresh streak
judged on its own results. Decay never runs for a key with requests
still in flight (the requested-set skip), so this cannot defeat the
in-flight deferral.

* fix(cli): mark suppressed stateful replays for the daemon batch decay (#9450)

A MIXED daemon batch — a suppressed task_list replay alongside at least
one executable call — wiped the frozen-board streak: requestedStatefulKeys
is built from executable calls only, the batch is non-empty so the
empty-batch early return does not apply, and once the prior result's mark
was consumed the replayed key sat in neither skip set, so
decayAbandonedDaemonStreaks zeroed the streak and recomputed
statefulMaxResultRepeat to 0. With wipes landing every few executed polls
the stuck signal never reached GLOBAL_DUPLICATE_THRESHOLD and the
adaptive-cap halt never fired under CLI-default skipLoopDetection=true —
the two runtimes drifted (core marks suppression via
noteSuppressedToolCallByCallId; issue #9450 requirement #6).

Mirror core's suppression mark in pushDuplicateBatch: add the replayed
stateful key to statefulResultKeysSinceLastBatch before
recordDaemonToolCalls runs, and add a mixed-batch variant of the
replay-interleave regression test.

* fix(cli): exclude never-executed skipped-output synthetics from the loop guards (#9450)

nonInteractiveCli's sibling suppression fabricates skipped-output
responses for unexecuted calls (a structured_output call sharing a batch
with task_list polls in a --json-schema headless run). The synthetic
parts carry the original callId, fail isDuplicateProviderToolCallResponse
and carry no executionStatus, so client.ts's recording feed paired them
with the streamed requests: the constant fabricated fingerprint counted
as a "changed" result every round, exonerating a stuck frozen-board
poller round after round. The daemon twin excludes exactly this class
(providerDuplicate / not_started filter) and agent-core excludes it via
neverExecutedCallIds.

Mark each unexecuted sibling call via
LoopDetectionService.noteSuppressedToolCallByCallId at synthesis time:
the request-side reservations unwind and the later fabricated response
finds no pairing, mirroring the daemon's not_started filter (issue #9450
requirement #6).

* fix(core): reduce digest-carrying stub shapes the prefix list does not enumerate (#9450)

* fix(cli): unwind the loop-guard reservations for plan-mode sibling skips (#9450)

* fix(cli): protect the suppressed replay key across the next daemon batch boundary (#9450)

* fix(core): keep the loop attribution on the final agent task card (#9450)

* fix(core): return the full digest line from degenerate batch-budget fits (#9450)

* fix(core): carry stateful streak marks across replay-suppressed rounds (#9450)

* fix(core): keep rejected stateful calls counting toward the consecutive-identical guard (#9450)

* fix(cli): unwind never-executed not_started synthetics before the interactive feed (#9450)

* fix(core): clear stateful loop-guard trackers on model fallback (#9450)

* fix(core): keep suppressed request counts when decay keeps the streak (#9450)

* fix(core): share one stub grammar between fitText and the loop guards (#9450)

* test(core): realign the replay-in-streak halt pin with the kept suppression count (#9450)

The keep-suppressed-counts fix replaced the suppression unwind of the
consecutive-identical increment: a suppressed call now keeps its
request-side increment and is counted into the streak's
suppressedRequests, which the exoneration gate subtracts from the
expected results. The replayed poll therefore still counts toward the
threshold, so the frozen-streak halt lands when poll_4 streams in as
the fifth identical request — one round earlier than under the unwind
the previous pin tracked: expectedResults is 5 - 1 in flight (poll_4)
- 1 suppressed (the replay) = 3, exactly the three unchanged
frozen-board results recorded for poll_1..poll_3, so the halt is
corroborated by executed evidence at three executions. Mutation
checks: reverting to unwind semantics moves the halt back to poll_5
(four executions), and recording the fabricated replay response as a
result disarms the halt entirely (five executions).

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

* fix(cli): carry live stateful streak marks across non-stateful replay suppressions (#9450)

The daemon's replay-suppression marks (pushDuplicateBatch /
emitDuplicateBatch) re-added the replayed key only when the replay
itself was stateful, while core's twin
(carryStatefulStreakMarksAcrossSuppression) re-adds every live streak
key on ANY replay suppression. A MIXED batch whose suppressed replay
is NON-stateful (the provider re-emitting an already-handled
read_file-style call id alongside an executable call) therefore left
the polled task_list key in neither skip set once the previous poll's
result mark was consumed: decayAbandonedDaemonStreaks wiped the live
frozen-board streak at the next boundary and recomputed
statefulMaxResultRepeat toward zero. Under the CLI default
skipLoopDetection=true — where the cap's stateful stuck signal is the
only live halt path — replays interleaved at <=5-poll intervals kept
the peak below GLOBAL_DUPLICATE_THRESHOLD indefinitely and the turn
ran toward the hard backstop, while core halts the identical event
sequence just past the soft cap (requirement #6).

Mirror core's carry: on any replay suppression, re-add every
statefulResultStreaks key with consecutiveIdenticalResults > 0 to
statefulResultKeysSinceLastBatch, in both pushDuplicateBatch (the
batch's own boundary) and emitDuplicateBatch (the next boundary).
Decayed streaks carry zero, so the carry only postpones an imminent
decay by one boundary — it never resurrects an abandoned peak. Adds a
Session.test.ts interleaving pinning the exact mixed non-stateful
replay shape (frozen board, poll every 3rd round, halt at
totalToolCalls 21); the pin fails with the carry reverted.

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

* refactor(core): trim PR back to the core result-aware loop-guard fix (#9450)

The PR accreted 53 commits / +7634 lines over a week of review rounds,
each adding speculative edge-case guards and tests far beyond the
reported issue. This commit restores the PR to the original fix scope:

- Keep the result-aware duplicate-detection core in loopDetectionService
  plus its attribution plumbing (client, agent-core, agent-headless,
  agent-interactive, agent-events, telemetry) and the matching tests,
  three-way merged onto current main.
- Revert the 17 files that only carried accreted hardening (daemon
  Session path, truncation fingerprinting, tool-response-finalizer,
  synthetic-result exclusions, batch-budget fitting, decay logic, and
  their tests) to the main baseline.

Net PR diff: 10 files, +1051/-14 (was 27 files, +7634/-44).

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

* fix(core): count consecutive results for stateful poll loop guards (#9450)

The result-aware counting for stateful read tools accumulated turn-wide
(call, result fingerprint) pair totals, contradicting the invariant its
own comment states: the same call returning changed state is productive
and must not accumulate toward either halt. A board oscillating between
two byte-identical states repeats each (call, result) pair across the
turn, so it halted with GLOBAL_TOOL_CALL_DUPLICATE on the 11th poll with
heuristics on, and — under the CLI default (skipLoopDetection) — the
pair totals fed capMaxKeyRepeat until the always-on adaptive cap halted
with TURN_TOOL_CALL_CAP past the soft cap.

Count consecutive identical results per repeat key instead: the count
restarts at 1 whenever the result differs from its predecessor, so an
oscillating board is changed-state progress on every poll in both modes
while a frozen board still accumulates even when interleaved with other
calls. Adds regression tests pinning oscillating-board survival in both
modes and the interleaved frozen-board halt via the adaptive cap.

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

* fix(core): restore the trimmed verification findings for stateful poll guards (#9450)

The sandboxed verification of the trimmed PR reported four findings; this
restores the three behavior fixes it proved live and pins the four coverage
gaps its mutation matrix exposed:

- Oversized (persisted) results escaped every result-aware guard: the
  stub envelope embeds a per-call unique path, so hashing it verbatim
  fingerprinted uniquely every poll and a frozen board read as "changed
  every time" (probe P2: no halt in 12 polls vs base halting at 5).
  buildStub now embeds a sha256 of the full pre-truncation output
  ("Full output sha256: <hex>"), and the guard's fingerprint reduces
  leading-producer stubs to that digest — with a path-free preview/
  truncated-part payload fallback for digest-less stubs and verbatim
  treatment of non-stub text (a mid-content quoted marker never matches:
  recognition is prefix-gated and the digest label must start its line).
  Frozen oversized boards halt at the unchanged threshold regardless of
  the per-call path; mutations past the preview window stay visible.
- Provider-duplicate call ids halted a productive poller fail-safe (probe
  P3): request counts fed pre-dedup while results land once per deduped
  executed call. Both reasoning-loop owners now feed the guards one
  ToolCallRequest per call id per attempt (a per-attempt Set mirroring
  dedupeToolCallsById; id-less calls never deduped), cleared on
  retry/fallback — agent-core's subagent stream loop and client.ts's
  main-session stream.
- Coverage pins (finding F4): restored the client.ts recording-wiring
  tests (frozen halt, changed survival, duplicate-id population parity);
  added QwenLogger loop_type journal tests, the interactive stop-message
  attribution test, and the partial-evidence fail-safe fixture.

Verification: packages/core tsc clean; loopDetectionService 145/145,
agent-headless 68/68, client 370/370, agent-interactive 25/25,
qwen-logger 43/43, truncation/shell/finalizer 368/368; prettier/eslint
clean on touched files.

---------

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>
* fix(serve): classify channel initialization timeouts

* fix(serve): scope the init-timeout safe-retry contract to plain creation

The initialize-timeout mapping in sendBridgeError promised
retryable:true / sideEffectPossible:false for every route, but two
paths mutate before or around the initialize handshake:

- POST /session with branch/worktree runs createBranch (moving the
  shared HEAD) or creates a worktree before spawn, and the rollback on
  failure is best-effort — a failed checkout rollback leaves the repo
  on the new branch while the response claims no side effect.
- POST /session/:id/branch and /side-task can time out on a
  replacement channel's initialize after the fork was already durably
  committed; a contract-trusting retry would commit a duplicate fork.

The safe-retry shape is now emitted only when the caller asserts via
the new initPrecedesMutations context flag that initialization
strictly precedes every durable mutation — POST /session sets it only
when no branch/worktree was prepared. All other paths keep the typed
init_timeout code, phase, and timeoutMs but omit Retry-After,
retryable, and sideEffectPossible, reporting an unknown outcome.

The protocol doc narrows the authoritative claim accordingly and
notes that timeoutMs reflects the configured --initialize-timeout-ms
budget (values shown are the default). Tests pin the branch-body
reduced shape (red without the route guard) and the non-initialize
label falling through to the generic 500.

* docs(serve): clarify init_timeout 504 on load/resume routes

The restore Errors list previously described every 504 as
session_restore_timeout (retryable, fenced), but an init-budget
expiry during ensureChannel returns init_timeout without Retry-After,
retryable, or fence — the restore was never dispatched. Add the
init_timeout 504 entry and narrow the "any other route" paragraph
to mutation-bearing routes, explicitly calling out load/resume.

* fix(cli): remove duplicate BridgeTimeoutError import in error-response

The file imported BridgeTimeoutError from both
@qwen-code/acp-bridge/status and ../acp-session-bridge.js,
which caused a TS2300 duplicate identifier build failure.
Drop the bridge/status import; the local acp-session-bridge
re-export is the one used across the route error handling.

* fix(cli): reconcile init_timeout tests and docs with merged #10268 behavior

Update error-response and server tests plus the qwen-serve protocol doc
to reflect that `newSession` dispatch timeouts now map to a retryable
`init_timeout` 504, while `initialize` timeouts without caller context
use the reduced contract.
…ller prose (#10587)

* fix(review): readable bilingual disclosures for lint deferrals and caller prose

The review body's disclosure sentences carried two readability defects,
both visible on PR #10567's posted round-1 body:

- The deferred-checker line stuttered: script-lint's deferral/skip reasons
  ended with an "— not linted" tail written for a standalone context, the
  gate spliced them under a "the executable-script lint —" prefix, and the
  body wrapped the result in a sentence that already opens "Not linted:".
  Posted: "Not linted (tool limitation, not a blocker): the
  executable-script lint — ... — not linted." The reasons drop the tail,
  and the disclosure drops the circular prefix — the wrapper names the
  fact once, the path and reason carry the rest.

- The body's Chinese half presented untranslated English as its
  translation. Two legs:

  - The deferral disclosure is machine-built from the report, so it can
    carry a real translation: `scriptLintGate` now returns bilingual
    disclosure pairs, the report schema gains an optional `reasonZh`
    (the actionlint deferral supplies it), and an older CLI's report
    without one falls back to the English reason in both halves.

  - Caller-prose "Not reviewed" entries stay untranslatable by
    construction, and the Chinese label now says so —
    `未审查(原文为英文):` — instead of presenting an all-English
    sentence as a translation; the payload keeps its own English full
    stop rather than closing an English sentence with `。`.

* test(review): pin the bilingual deferral disclosures the review asked for

Five review-suggested pins, each verified by re-running its witness
mutation:

- The comment-grammar fixture now carries a malicious reasonZh, so the
  stripCommentGrammar(d.reasonZh) leg is exercised against a live marker
  (deleting the call previously survived the suite).
- The pipeline reasonZh is pinned to its Chinese literal — a
  toContain('actionlint') fragment was satisfied by the English reason too.
- Both skipped reasons get not.toContain('not linted') pins; re-appending
  either tail previously shipped green.
- A two-entry deferred fixture pins the en '; ' and zh ';' joins and the
  reasonZh-carrying branch end-to-end — every prior fixture held one entry,
  so no join separator was ever observable, and the only full-sentence zh
  pin exercised the English-fallback branch.
* fix(hooks): close four trust-boundary holes in hook execution

HTTP hooks never follow redirects (a 3xx is a non-blocking failure and
the target is never contacted). A workspace may narrow the HTTP-hook URL
whitelist but never replace one set at User, System, or SystemDefaults
scope. Qwen-internal secrets are never substituted into settings, hook
URLs/headers, or channel configuration, compared case-insensitively for
Windows, and the external-tool guard token joins the denylist; the CLI's
duplicate settings resolver is replaced by a core subpath export so the
serve fast path stays light. Project-level skill and subagent side
effects (allowedTools grants and frontmatter hooks) require a trusted
folder, mirroring the settings-file project-hooks gate.

Replaces #8396.

* fix(hooks): re-apply skill side effects when trust is granted mid-session

The Skill tool's "already loaded" dedup return sat above the new trust
gate, so a project skill first invoked in an untrusted folder kept its
allowedTools and hooks unapplied for the whole session even after the
folder was trusted. The gate is now re-evaluated on every invocation;
both grants dedup, so re-applying is idempotent.

Also keep an own `__proto__` variable in the sanitized child env (spread
and delete instead of an assignment loop), correct the guard-token note
in the denylist rationale (the ACP child never receives it), and pin the
settings-layer and serve fast-path secret refusal with tests.

* fix(hooks): give the project-skill trust gate a removal side

The gate on a project skill's side effects was enforced only when they were applied: once a repo-supplied skill had registered its frontmatter hooks and granted its allowedTools as session allow rules, a folder trust revoked mid-session (an IDE trust notification flips it live) left both active until the session restarted. Rather than tracking which skills applied what and unregistering on a trust event, the grants are marked trust-gated and the two consumers re-read folder trust at decision time: the hook event handler skips a trust-gated session hook at fire time while the folder is untrusted, and the permission manager leaves a trust-gated session allow rule out of every decision (and the effective-rules listing) while it is. Trust granted again restores both, symmetric with the mid-session grant the gate already honoured, and nothing changes on rounds that registered no project-skill hook.

The per-invocation re-application of side effects also turned two WARN lines into the normal steady state of a trusted skill — a dedup returning zero and a hookless skill — so both are debug-level now, reworded to say what the zero means.

* fix(permissions): let an ungated same-raw grant clear the kept entry's trust gate

The session-allow dedup kept the first entry's trustGated flag, so a
user-level grant of a raw the project skill had already granted would
inherit the repo grant's suspension when folder trust is revoked. The
dedup now keeps the wider grant: an ungated arrival clears the flag; a
gated re-arrival (skill reload) stays an idempotent skip and never
re-gates a rule the user holds.

Also pins the slash-command path's grant payload in the
SkillCommandLoader trusted-folder test to exactly { trustGated: true },
so shipping the grants ungated reddens the suite instead of passing on
call counts alone.

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
…ences of bound PRs (#10425)

* feat(web-shell): derive session issue bindings from the closing references of bound PRs

Sessions could be found by the PR they produced, but not by the issue they were working on — the maintainer flow starts from an issue and ends in a PR. GitHub already knows that link: the PR body's "Fixes #N" is its closing reference. Snapshot those references, with the issue state, onto each bound PR entry in the existing sidecar instead of adding a client-side binding, a second sidecar, or a prompt/branch heuristic that would misbind across the shared PR/issue number space.

The refresh sweep gains a second, by-number GraphQL lookup: every non-merged binding (closing references change while a PR is open) and every merged binding that predates the snapshot (one catch-up) is queried in batches, and the result is written in place together with the PR state — order and createdAt untouched, url-mismatched (foreign repository) entries never touched, unchanged sidecars never rewritten. The slim PR list query stays as it was: nesting closing references under it measurably slows it and still carries no issue state, while a by-number lookup also reaches PRs outside the 500-entry window. gh exits non-zero over a NOT_FOUND alias yet still prints the other aliases, so the wrapper parses that payload rather than failing the batch.

Every projection from the sidecar to the wire now goes through one shared helper, the bridge keeps the daemon-derived snapshot across a client re-bind, and the SDK guard validates issue entries with the same url rules as PRs. The Web Shell tooltip lists the issues under the PR rows (deduped across stacked PRs) with GitHub-style state icons, and sidebar search matches an issue number with or without the hash.

* fix(serve): converge foreign merged bindings and gate every snapshot carry on the PR's url

Review round 1 on the issue-snapshot sweep. A merged binding the repository cannot resolve — another repository's same-numbered PR — could never receive a snapshot, so it re-entered the by-number lookup on every sweep and broke the "all merged with snapshots costs no call" invariant; the sweep now writes it a converging empty snapshot once the lookup has succeeded, and only ever stores issues the lookup actually resolved for that url. The session-list merge of persisted and live bindings compared numbers alone, the one carry site that did not check the canonical url, so a cross-repository re-bind could briefly wear the previous repository's issues; it now applies the same url gate as the sidecar and bridge. The closing-references fetch bound and the sidecar's per-PR cap were two literals tied by a comment (raising one would void every sidecar on read); they are one constant now, declared in the utils layer the sidecar service imports. The GraphQL wrapper passes its own timeout to the error formatter, and the write-side binding types on the bridge and the SDK omit issues so a client-bound issue list is a compile error, matching the runtime drop.

Tests pin each of these plus the gaps the review found: the killed-timeout message, the list-query failure still snapshotting issues, client-supplied issues dropped by the bridge, the cross-repository re-bind dropping the snapshot, a closed issue with no state reason mapping to completed, the open issue-state rendering, the full `-f query=` argument, and the SDK guard's number and non-object checks.

* fix(serve): retire unresolvable bindings from the issue sweep and reject non-safe PR numbers

Review round 2 on the issue-snapshot sweep. A merged binding no lookup can ever snapshot now converges in every case where that is knowable — the lookup succeeded without resolving it, the lookup is structurally impossible (no gh binary, no git root), or the platform has no closing references at all (Aone) — while a transient failure still retries, since it says nothing about the PR's references. A non-merged binding to another repository stays out of the lookup altogether: the list query names this workspace's repository, and a binding outside that canonical prefix can never resolve here, the GitHub twin of the Aone refreshability filter. A workspace with only unresolvable bindings therefore costs nothing after one sweep, and a mixed workspace still refreshes its open bindings' state.

The GraphQL wrapper accepts safe integers only: a positive integer-valued double at or beyond 1e21 stringifies in exponential notation, an invalid Int literal that failed the whole document instead of one alias — one such binding would have disabled issue refresh for the entire workspace. The session-list merge finds the persisted entry by url rather than a last-wins number map, so a hand-edited sidecar with two same-numbered entries cannot shadow the live binding's own snapshot.

Tests cover each path (lookup resolving nothing, foreign open binding never queried, gh unavailable converging, transient failure retrying, mixed workspace running the list query, generation closing between the two queries, merged-only Aone workspace going quiet, the repository scope in the query, and the safe-integer bound), and the E2E plan's bridge filter now selects the client-supplied-issues test.

* fix(serve): reject partial GraphQL errors and match issue lookups by PR identity

Review round 3. The response parser read only the repository data and dropped the top-level GraphQL errors, so a server error nulling one alias, or a sub-field error nulling a resolved PR's closing references, came back as a successful lookup — and the sweep then retired a merged binding on that "absence" with a permanent empty snapshot. Only an alias-level NOT_FOUND now counts as absence; every other partial error fails the call, which keeps retrying. A repository gh cannot resolve at all (no git remotes, or none on a GitHub host) is a structural failure, not a transient one, and converges like a missing gh binary; execFile's error carries no stderr of its own, so the wrapper attaches it before classifying.

Lookup results are matched to bindings by host, owner, repository, and number rather than by canonical url, so a binding spelled with `www.`, `http:`, or a `/files` suffix receives the issues fetched for it — written under the binding's own url, which the sidecar's canonical write gate requires — instead of being mistaken for a foreign repository and retired empty. The SDK guard caps issue lists at the sidecar's ten, matching the design.

Tests: partial-error rejection with the NOT_FOUND tolerance kept, the repository-unresolved kind, the variant-url binding keeping its fetched issues, convergence under not_a_repo and repo_unresolved, a foreign merged binding converging during a lookup outage, the Aone workspace never reaching the GitHub lookup and never re-resolving its origin once quiet, the canonical-equal url spelling inheriting the sidecar snapshot in the session list, the sanitized env dropping GH_REPO, the exact safe-integer literal, and a state-less issue accepted by the SDK guard.

* fix(core): treat a repository GitHub no longer serves as structurally unresolved

Review round 4. A renamed, deleted, or access-revoked repository answers the lookup with a NOT_FOUND on the repository itself rather than on an alias; that was thrown as a partial error and reported as a transient failure, so merged bindings in such a workspace never converged and the sweep kept paying the failing lookup. It now classifies as the same structural kind as a missing GitHub remote, which the sweep already retires bindings on.

Test strengthening the round asked for: one fixture per accepted gh diagnostic, the repository-level NOT_FOUND payload, the SDK guard accepting exactly ten issues and rejecting a control character in an issue url, and the tooltip fixture reworked so binding order contradicts number order, a filtered PR's issues disappear with it, a same-numbered issue from another repository stays distinct, the merged PR contributes an observable issue, non-openable issues leave no text behind, and the state-less icon check reads the class token list instead of jsdom's SVGAnimatedString.

* fix(serve): keep GH_HOST mismatches transient and match every GitHub PR url spelling

Review round 5. gh's "error parsing owner value" prefix also fronts its GH_HOST-mismatch diagnostic — an environment problem, not a missing repository — so the structural classification is narrowed to the two wordings that really mean no usable GitHub remote, and the mismatch stays a transient failure that never retires bindings. PR identity now also recognizes the `.diff` and `.patch` spellings GitHub serves, so such a binding receives its fetched issues instead of a permanent empty snapshot. And the write under a binding's own url no longer waits for a successful issue lookup: during an outage the list query's state still lands on a non-canonically spelled binding instead of being dropped by the sidecar's canonical gate.

Tests: the GH_HOST diagnostic stays `failed`, the three structural wordings converge, `.diff` and `.patch` bindings keep their fetched issues alongside the `/files` spelling, and a `www.`/`/files`-spelled open binding turns merged while the lookup is failing.
…10066)

* feat(serve): allow relocating session attachment storage via env var

Adds QWEN_SERVE_SESSION_ATTACHMENTS_ROOT, which stores session attachments
under <root>/<projectHash>/attachments instead of the runtime temp dir so
operators can pin them to a dedicated volume. Reads and removes that miss
the configured root fall back to the default dir so pre-switch attachments
stay readable and removable; archive cleanup removes both roots. New uploads
never shadow a legacy fallback name, and both roots are removed via the same
tombstone dance so a deletion racing a session restore cannot sweep up a
successor directory.

* fix(serve): keep attachment root resolver off fast path

* fix(serve): harden session attachment fallback against degraded roots

read() and remove() no longer force-create the configured root before
consulting the fallback, so a degraded configured volume serves and
removes pre-switch attachments from the healthy default dir instead of
failing; delete() removes the fallback root first, mirroring remove(),
so a failure on the legacy root keeps the primary copy intact;
QWEN_SERVE_SESSION_ATTACHMENTS_ROOT is trimmed before use. Corrects the
docs to say attachment cleanup happens on session delete, not archive.

* fix(serve): address round-2 review findings on session attachment storage (#10066)

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

* fix(cli): stop restore-probe tests depending on ambient host git state (#10066)

* fix(acp): stabilize attachment fallback handling

* fix(acp): skip attachments being removed during copy

---------

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: wenshao <shaojin.wensj@alibaba-inc.com>
…34 (#10633)

`metadata.autoOrient` only exists in sharp >= 0.34. On installs that
must stay on older sharp releases (for example because newer prebuilt
libvips cannot load on the host libc), `read_file` image views and
`zoom_image` crash on every image with "Cannot read properties of
undefined (reading 'width')".

- Add `orientedSize()`: prefer `metadata.autoOrient` when present,
  otherwise derive the oriented size from the stored dimensions and
  the EXIF orientation tag (orientations 5-8 swap the stored axes).
- Apply EXIF orientation with `.rotate()` instead of the `autoOrient`
  constructor option, which does not exist before sharp 0.33. Both
  forms behave identically across the old and new sharp series,
  including extract-in-oriented-space semantics.

Covered by orientedSize unit tests and EXIF-rotated overview/crop
integration tests.
* feat(channels): show tool details in permission requests

* feat(channels): Improve DingTalk error fallbacks

* fix(channels): Harden fallback message details

* fix(channels): Correct misclassified inbound errors and pin classifier branches

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
* feat(cli): OpenTUI foundation modules — theme, a11y, clipboard, keys, dialogs scaffolding

Foundation batch of the OpenTUI migration tracked in #8662. Adds the renderer-neutral foundation modules under ui/opentui: theme family, a11y (plain-text, screen-reader), clipboard, key-map, mouse hit/caret, link-click + osc8 parity, early-input, exit guard/lifecycle, kitty negotiation, event-adapter, item-projection, slash dispatch (+ command parsing), commands context/output, help content, input history, and the dialog scaffolding primitives (core/shared) with the theme dialog. Two helpers land inside ui/opentui rather than utils/ to respect the utils leaf-layer rule (#9737). Stacked on the infra batch: consumes ui/model streaming model and @OpenTui deps. No reachable ink code changes beyond a one-line export addition in the shared osc8 module.

* fix(cli): import originals instead of forking slash parser and dialog scope utils

* fix(cli): address R1 review findings in OpenTUI foundation modules

* fix(cli): align OpenTUI command host with the memory-file-count rename

Upstream renamed setGeminiMdFileCount to setMemoryFileCount in the command
UI contract; the rebase onto main surfaced the mismatch at build. Rename
the host interface member, the bridge wiring, the dispatch stub, and the
test mock to match.

* feat(cli): OpenTUI migration live-session and input batch

Third landing batch of the OpenTUI migration (#8662): live-session stream
fold and model, message rendering (markdown heal, MCP progressive, client
tool runs, text batching), transcript adapter with resume/session-switch,
sticky todos, the composer (input-prompt view/key/model), mouse rows and
scrollbar, unified-diff rendering, and session-compaction notice. All
additive — no reachable ink code path is touched, ink remains the default.

Carries the first consumer of the remend dependency deferred from the
infra batch, placed in devDependencies per the renderer-deps convention.
The stacked-skill completion helpers import from the relocated
ui/commands module following the upstream rename.

* fix(cli): address R2 review findings in OpenTUI foundation modules

Round-2 review fixes (17 Critical + 10 Suggestion resolved in code):

- dialogs-shared: move number-select flush out of the setState updater
  (StrictMode double-fires onSelect); split setActiveIndex (ink
  SET_ACTIVE_INDEX, lands on any in-range row) from highlightIndex
  (arrow keys skip disabled rows) so wheel navigation never sticks
- event-adapter: chat_compressed notice mirrors ink formatCount ('~'
  prefix for estimated counts); vision_bridge_notice renders
  summary\nnotice; explicit projections for task_execution /
  findings_list / terminal_image keep multi-MB payloads off the
  transcript; retry-countdown-clear forwards isContinuation
- slash-dispatch: isSlashCommandInput drops the '?' branch (ink gate
  routes ? input to the model); executeSlashCommand races the action
  against the abort signal; dialog effects carry the
  OpenDialogActionReturn payload; projected added-item text surfaces
  alongside non-handled effects (notice); message-shaped items project
  to their text; ui.history comes from env; absent sessionStats stamp
  now, not epoch; telemetry parity (recordSkillInvocation /
  recordAutoSkillCommandUsage / makeSlashCommandEvent)
- item-projection: model stats render per-(model,source) sections with
  N/A for unpriced entries; Tool Calls line uses ASCII x like ink;
  redactProxy deduplicated via systemInfoFields export
- theme: palette/syntax colors resolve through color-utils toHex before
  parseColor (ink CSS names / *bright names no longer degrade to
  magenta); unresolvable values stay unset
- key-map: kitty 'kpenter' normalizes to 'return'; resolveCommands
  exposes ink's key fan-out (Ctrl+C fires QUIT + CLEAR_INPUT)
- a11y: hardWrap delegates to wrap-ansi (word-boundary parity with
  ink's screen-reader path); markdown reducer tracks fence length,
  keeps fence-like lines literal inside fences and inner backticks in
  multi-backtick spans; stripAnsi delegates to strip-ansi plus a
  private-parameter CSI pass (SGR mouse, DEC save/restore)
- clipboard: OSC 52 write gated on a TTY (stderr preferred), tests spy
  the stream instead of writing real sequences to the runner's terminal
- exit-guard: independent per-key arm windows like ink
- dialogs-theme: diff preview pane receives syntaxStyle/filetype

* fix(cli): harden kitty probe and screen-reader writer per maintainer review

- kitty-negotiation: KITTY_REPLY_RE requires at least one flag digit
  (\d+), so an echoed bare query \x1b[?u in PTY/CI environments no
  longer resolves true and locks the renderer into kitty mode on a
  terminal that never answers queries; the accumulation buffer keeps
  only a 256-byte tail (bounded memory, bounded rescan under byte
  floods); the settle-window drain is removed — an EventEmitter data
  listener cannot consume chunks from other listeners, so late replies
  flow to the renderer's input parser like any other terminal noise
- a11y-screen-reader: ScreenReaderOutputWriter sanitizes written
  content (stripAnsi + drop bare C0/C1 controls, keep newlines) so the
  plain-text-only contract is enforced at the writer instead of
  trusting every future caller — smuggled OSC 52 clipboard writes or
  title/cursor sequences cannot execute on the main screen

* fix(cli): address ytahdn independent review findings in OpenTUI foundation

All 15 findings from the independent static review verified in source and
fixed (no false positives; none deferred):

- quit effect carries QuitActionReturn.messages projected to text on a
  notice field — ink renders them via QuittingDisplay and the payload was
  permanently lost (Important #1)
- error and finished branches emit retry-countdown-clear like ink's
  handleErrorEvent/handleFinishedEvent, so a terminal event inside the
  countdown window no longer leaves a stale retry row (#2)
- projectContextUsage renders the compaction-threshold ladder and the
  per-item detail sections (tools/memory/skills, ink's sort order) when
  showDetails is on — /context detail transcripts no longer show strictly
  less than the compact view (#3)
- projectMcpStatus honors showSchema (parameter JSON under each tool) and
  showTips, so /mcp schema is distinguishable from /mcp (#4)
- SlashDispatchEnv.settings is required: the real
  CommandContext.services.settings is non-null and a null surfaced as a
  generic command failure on first .merged read (#5)
- dead singleColumn flag removed from the help width layout (clamp makes
  it always false; ink has no single-column mode) (#6)
- truncated SS3 tail (bare ESC O) is stripped like the truncated CSI
  tail, so a captured half F1-F4 no longer leaks 'O' into the composer (#7)
- readBufferRow trims cellColumns alongside text, so URLs ending at
  end-of-row on a wide character hit-test on both halves of the cell (#8)
- kitty probe writes guarded: a synchronous stream throw settles the
  probe (restores raw mode, removes the listener) instead of leaking (#9)
- eraseLines reuses the ansi-escapes helper (already a repo dependency)
  instead of a byte-identical hand-rolled copy (#10)
- truncateText passthrough wrapper dropped; callers use the exported
  truncateHelpText directly (#11)
- SkillsList truncate keeps total length n like ink, so the description
  column no longer shifts by one cell when a name truncates (#12)
- model_fallback names pass through sanitizeDisplayText like ink (#13)
- selectIndex fires onHighlight before onSelect (ink dispatches
  SET_ACTIVE_INDEX then SELECT_CURRENT), keeping highlight-driven
  stay-open dialogs synced on mouse input (#14)
- mcp_app without fallbackText renders empty instead of JSON-dumping the
  embedded HTML; projectAbout hides Base URL when selectedAuthType is
  empty, matching ink's formatBaseUrl (#15)

* fix(cli): address R3 review findings in OpenTUI foundation modules

- executeSlashCommand catch checks the abort signal first: an ESC-
  cancelled command (action rejects AbortError) returns handled with no
  failure telemetry or error message, mirroring ink's processor — the
  race promise never resolves when the signal is already aborted at
  addEventListener time
- submit effect carries the full SubmitPromptActionReturn contract
  (modelOverride, onComplete, refreshContextFilesOnWrite) so the
  backend can honor /model <id> <prompt>, /dream's manual-run record,
  and /remember's context refresh like ink instead of silently
  degrading them
- closing fences cannot carry info text (CommonMark): a ```js line
  inside an open block is literal body, not an early close that drops
  the block and inverts parse state for the rest of the document
- info items append their linkUrl/linkText footer (ink's InfoMessage
  renders it; headless/SSH users need the printed URL, e.g. /bug)
- the screen-reader sanitize keeps TAB: it separates words in
  tool/model output and deleting it fused adjacent tokens

* fix(cli): address R4 review findings in OpenTUI foundation modules

- a11y-plain-text: split on all CommonMark line endings so CRLF
  markdown opens/closes fences correctly; private-param CSI regex
  covers ECMA-48 intermediate bytes; DCS/SOS/PM/APC and unterminated
  OSC sequences consumed before strip-ansi; code-span pattern mirrors
  ink's INLINE_CODE_SPAN_PATTERN (non-empty content, closing-run
  lookbehind)
- dialogs-shared: clearNumberBuffer called from setActiveIndex,
  selectIndex, and resyncKey block so wheel/hover/click/resync can't
  commit a stale numeric-flush selection the user never made
- event-adapter: tool_call_response carries visionBridgeNotice on the
  tool-result event (ink ToolMessage renders the egress disclosure)
- item-projection: projectContextUsage reads memoryFiles as { path,
  tokens } (ContextMemoryDetail), not { name, tokens }
- key-map: 10 kp* keypad-navigation aliases (kpleft→left, …) and
  super flag folded into meta (ink Cmd+Enter = newline, not submit)
- link-click: cellColumns no longer truncated to trimmed text length
  (preserves the wide-glyph right-half boundary); findUrlAtRow end
  boundary is width-aware (stringWidth of the last glyph)
- slash-dispatch: submit effect carries PartListUnion content + a
  textContent string for text-only consumers (image parts survive);
  toggleVimEnabled and startNewSession seams wired from env; abort
  race resolves immediately for an already-aborted signal
- clipboard: OSC 52 self-write removed — copyToClipboard's existing
  fallback (writeOsc52 / wrapForMultiplexer) is the single source
- a11y-screen-reader: appendStatic skips clean === '\n' (ink's
  hasStaticOutput guard)

* fix(cli): address #10383 R1 findings in foundation modules

- event-adapter: finished branch emits retry-countdown-clear BEFORE the
  info notice so the countdown row is actually cleared (the fold only
  pops when the last item is the retry row)
- item-projection: /mcp tips now include all 5 lines ink renders (added
  OAuth auth tip and Ctrl+T toggle tip)
- link-click: wide-glyph end boundary uses the last code point (not
  UTF-16 code unit) so non-BMP emoji are measured correctly by
  stringWidth
- a11y-plain-text: CSI_SEQUENCE replaces PRIVATE_PARAM_CSI — drops the
  marker requirement so any CSI (with or without private parameter
  marker, with or without intermediate bytes) is fully consumed

* fix(cli): address #10383 R2 Critical findings in slash-dispatch

- abort race: already-aborted signal now skips command.action entirely
  (result = undefined) instead of eagerly evaluating it as a Promise.race
  argument — the action's side effects (clear, persist, addItem) must not
  run on a cancelled submission
- parent command telemetry: logEvent (slash_command SUCCESS) is now
  called before the early return for parent commands with subCommands
  (help listing) and bare handled — matching ink's finally-block logging

* fix(cli): address #10368 R2 review findings in live-session batch

- input-prompt: convert OpenTUI display-width cursor coordinates to
  code-point positions at the component boundary — the pinned
  @opentui/core reports logicalCursor.col/offset and el.cursorOffset in
  terminal-cell units (edit-buffer.zig), while the ported ink helpers
  work in code points; wide characters previously shifted placeholder
  backspace, the backslash continuation check, completion targeting,
  and history edge compares
- input-prompt: bump both search sequence refs when Esc dismisses the
  completion dropdown so an in-flight search resolving afterwards cannot
  re-open it and hijack Enter
- live-session-model: carry the vision-bridge egress disclosure through
  the tool-result fold (ink ToolMessage renders it under the result)
- messages: recognize the producers' two-L 'cancelled' summary spelling
  so canceled tools get the CANCELED glyph with strikethrough instead of
  the red ERROR glyph
- session-switch: wrap /resume and /branch in the telemetry swap
  transaction (begin before the outgoing-session capture, commit at the
  UI re-key, abort after a rolled-back swap) — restores the usage
  aggregate on failed swaps and rejects concurrent switches
- tests: display-width fake editor, wide-char placeholder/continuation
  witnesses, Esc invalidation, fold notice, cancelled spelling, and the
  three swap-transaction lifecycle cases

* fix(cli): declare cursorOffset on the FakeEditor test interface

The display-width fake added in 656e996 implements a cursorOffset
getter/setter but the interface it is cast through never declared the
member, so tsc --build fails with TS2339 at the two reads in the
wide-char placeholder test. Typecheck ran before that commit's files
were staged and missed it.

* test(cli): stub listStartingRunIds in the session-switch fake registry

The workflow-run registry gained listStartingRunIds with the workflow
tasks feature on main; backgroundWorkUtils iterates it when describing
blocking work, so the fake registry in session-switch.test.ts now
implements it (empty) to match the interface the merged code expects.

* fix(opentui): address yiliang114 review findings (4 P2 + 1 P3)

- transcript-adapter: FIFO queue for id-less tool call pairing so
  tool-start and tool_result share the same minted id
- session-switch: move uiSwapped=true to right after startNewSession
  (the first irreversible host mutation) preventing core/UI divergence
  on mid-sequence throw; same fix for branch handler
- session-switch: add error item when /resume targets an unloadable
  session instead of returning silently
- diff-render: run diff content through escapeAnsiCtrlCodes matching
  the ink text-boundary convention (useTurnDiffs.ts)
- package.json: move remend from devDependencies to dependencies
  (imported from production source markdown-heal.ts)

* fix(transcript): address R6 review — thinking latch, cancelled status, text join

- Replace one-shot `closed` latch with `thinkingOpen` state so
  [thought, text, thought, ...] patterns emit matching thinking-end
  for each burst (P2)
- Mirror live path: treat cancelled tool status as failed, not ok (P3)
- Join user text parts with newline instead of empty string (P3)
- Regenerate package-lock.json so remend is in dependencies (P2)

* fix(opentui): address review-pr bot R3 critical findings

- session-compaction: add missing COMPRESSION_FAILED_EMPTY_SUMMARY,
  OUTPUT_TRUNCATED, and API_ERROR cases to match ink compression-text.ts
- live-session: distinguish cancelled from error in tool-end summary
  so toolStatusMeta renders strikethrough instead of red X
- transcript-adapter: gate slash_command replay on phase=invocation to
  prevent double-replay (recorder writes both invocation and result)
- input-prompt: add key.meta/key.option to DELETE_WORD_BACKWARD branch
  to match the guard condition that intercepts Alt+Backspace

* fix(opentui): address round-5 review findings R2-5 R3-2 R3-12 R4-1

R2-5: update session-compaction.test.ts to assert the three new
parity texts (EMPTY_SUMMARY, OUTPUT_TRUNCATED, API_ERROR) added in
63a7f3c; the old assertion that EMPTY_SUMMARY returned '' is stale.

R3-2: transcript-adapter replay producer folded cancelled tool status
into summary 'error' (red ✕) instead of 'cancelled' (strikethrough).
Add the cancelled branch to match live-session.ts.

R3-12: hidden slash-command invocations (hiddenInvocation: true for
/auth, /help, /settings, /status, bare /effort, /btw) replayed as
visible user rows and entered composer history. Gate them on the
hiddenInvocation flag in the invocation filter.

R4-1: modelOverride was only carried on the first UserQuery send;
ToolResult continuation sends omitted it, so a per-turn model override
silently reverted to the session default after the first tool batch.
Propagate modelOverride into every continuation send.
* fix(dingtalk): recover status cards after network failures

* fix(dingtalk): retry card creation and classify token errors by errcode

Address round-1 review findings on #10357:

- gettoken business errors are classified by errcode: only known
  credential/app errors (40001, 40013, 40089, 90002, 90003) are permanent;
  transient codes such as -1 (system busy) and 88 (throttled) stay retryable
  so card recovery keeps going instead of latching the card in Running.
- A retryable createAndDeliver failure now retries with the same capped
  backoff. Boundary decisions do not wait through the backoff (they fall back
  to text and the retry continues), and finalize/dispose abandon it.
- flush() no longer writes to a card whose stream latched a permanent
  failure, closing the stray-write window opened by create() returning true.
- The terminal statusLine is computed once at finalize time so retries do not
  inflate the elapsed seconds by the outage duration.
- Drop the redundant streamRetryTimer check in flush()'s re-arm; scheduleFlush
  already refuses to arm behind a retry timer.
- Tests pin the terminal retry, exponential backoff and its 30s cap, the drain
  failure fallback, the retry-timer clear, ensure() after dispose, record
  untracking, the permanent-failure boundary fallback at presenter level, the
  non-retryable delivery failure, and transient gettoken errcodes.

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

* fix(dingtalk): resync cards after client reconnects

* fix(dingtalk): stop retrying invalid app credentials

* test(dingtalk): cover card recovery invariants

* fix(dingtalk): prevent status card content rewind

* fix(dingtalk): avoid redundant status card flushes

* test(dingtalk): pin completed flush state

* fix(dingtalk): preserve status card recovery

* fix(dingtalk): abandon cards after fallback

* fix(dingtalk): close abandoned status cards safely

* test(dingtalk): pin status card abandon guards

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
* feat(web-shell): unblock git update on dirty working tree

The workspace "Update Project" action ran a plain git pull, so any
uncommitted changes left users with a raw dirty_working_tree error
and no way forward outside a terminal.

The pull endpoint accepts two opt-in resolutions, the two things a
user would do in a terminal: stash the local changes (including
untracked files) around the pull and restore them by identity, or
discard them and fast-forward. Both are refused while a merge,
rebase, cherry-pick, revert or am is parked in the worktree, the
discard is validated (fetch + ancestor check) before anything is
destroyed, and a failed stash pull aborts only the merge or rebase
it started before restoring the entry. The branch picker offers the
two resolutions inline when the plain pull is blocked, with the
destructive one behind a confirming click, and renders the daemon's
message for every other refusal.

Ambient git configuration, ignored-file semantics and concurrent
pulls are deliberately left to git; docs/design records each as a
non-goal.

* fix(core): close the stash and discard windows in the dirty-worktree pull

Address the round-1 review of the resolution flows, by identity rather
than by adding guards:

- The auto-stash is captured by provenance (the new entry carrying the
  auto-stash message, from a before/after listing), never as "the top of
  refs/stash", so a terminal push landing in the gap is left alone.
- The drop checks the SHA git reports as dropped; if the slots shifted
  under it, the other entry is stored back and ours is reported as kept.
- Failure recovery aborts a merge or rebase only when its MERGE_HEAD or
  rebase-*/onto points at the upstream tip the pull was integrating; the
  sequencer probe runs immediately before stash push and reset --hard.
- The stash flow always throws a typed pull_failed after aborting, also
  when nothing was stashed or git refused the stash itself, so the client
  shows git's reason instead of re-offering the same resolution.
- The force flow fetches with --prune, re-verifies the upstream, and
  integrates the validated tip with merge --ff-only @{upstream} instead
  of fetching again after the discard.
- Successful responses are path-redacted like the failures; the conflict
  result carries the stash SHA, which the popover now names.
- The popover keeps the panel on force_unsupported, keeps the restore
  warning across a reopen, only dismisses the panel for a branch creation
  that actually runs, and allows 420s for the multi-command flows.

Tests drive the concurrent interleavings deterministically through a
PATH git shim that injects a terminal's actions around one invocation.

* fix(core): make the dirty-worktree pull recovery provenance-based and best-effort

Address the round-2 review of the resolution flows:

- Failure recovery aborts a merge or rebase only when git itself created
  it: a pre-existing state makes `git pull` exit 128 before touching the
  tree, so only an exit of 1 with the state pointing at the integrated
  upstream tip identifies the pull's own. A same-tip merge a terminal
  parked meanwhile is left in place with its staged resolution.
- Every recovery step is best-effort: a failed probe or listing never
  turns a recovered repository into an unclassified error, the post-push
  re-listing failure points at the entry by its message, and a failed
  store-back after a drop shift names the displaced entry and the command
  that recovers it.
- Both flows fetch (`--prune`) before checking the upstream, so a pruned
  tracking ref heals when the remote branch exists again; a configured
  upstream whose branch is gone is a typed refusal, while a branch with
  no upstream keeps git's own message.
- The force flow fast-forwards to the validated SHA rather than the
  symbolic `@{upstream}`, which a concurrent fetch can move.
- Kept-entry notices always carry the SHA; the restored arm of a failed
  update carries the drop diagnostic; silent git failures name the usual
  lock-file cause; the popover budget covers the 16-command worst case.

* fix(core): recover killed pulls and type post-discard force failures

Address the round-3/4 review of the dirty-worktree pull flows:

- A pull that dies without a numeric exit code — the flow's own 30s
  timeout kill, or an external/OOM signal — may already have written its
  MERGE_HEAD, so it now counts like a conflicted exit for the abort
  decision; the tip-identity guard is unchanged, so a merge a terminal
  parked meanwhile is still left alone. Previously the pull's own merge
  stayed parked while the response claimed a full restore, and a later
  commit could finalize the stray merge and silently exclude the
  upstream's content from history.
- The force flow's final ff-only merge is wrapped: a refusal after the
  discard (e.g. a skip-worktree file the reset cannot clear) is a typed
  pull_failed instead of raw text the route would re-classify as
  dirty_working_tree, looping the panel on a discard that can never
  succeed.
- A successful stash pull that had to keep a stash entry (failed drop,
  slot shift, failed store-back) reports it structurally (stashKept +
  stashSha); the popover renders that notice as a sticky warning, since
  it is the only record of where the entries went.
- The re-list-failure test pins "HEAD never moved" against a captured
  headBefore instead of comparing rev-parse with itself.

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
* feat(channels): customize DWS task reactions

* fix(channels): preserve DWS reactions across retries

* docs(channels): document DWS task reactions

* fix(dws): avoid duplicate retry end reactions

* test(dws): add reaction lifecycle regression witnesses (#10610)

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
…10664)

"still aborts a picker that is genuinely in flight when the client
hangs up" failed on the main push lane (run 33451462501) with
"expected undefined to be true": under shared ECS host contention the
request handler did not reach the picker mock within the fixed 30 ms
sleep, so no AbortSignal was captured before the client aborted, and
the follow-up 60 ms sleep could never observe the abort.

Replace both fixed sleeps with the vi.waitFor polling already used
throughout this file: wait until the picker actually receives the
signal, hang up, then wait until the signal reports aborted. The
assertions stay identical — only the timing guesses go away.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(cli): show Goal objectives in session picker

* fix(core): read the Goal objective as lifecycle state, not an appended label

Review findings on #10295:

- R1-1 (Critical): the tail scan kept the last `objective` found on ANY
  `goal_state` line, so a `/goal clear` record — which persists `goal: null`
  and no objective at all — left the picker labelling the session with the
  goal the user had just cleared. `readLastMatchingLineFieldSync` reads the
  field from the LAST marker-carrying line instead, so the newest lifecycle
  record decides even when it omits the field. Its miss is now three-way:
  only `absent` (whole file scanned) lets the records fallback speak. A
  head-window hit is gone entirely — for a file bigger than the window it
  would resurrect a create record with an unknown number of later lifecycle
  records out of reach, and the parsed records are that same oldest slice of
  the file, so the honest answer there is no label.
- R1-5: the pasted-twice policy block is now one `resolveGoalObjective`
  helper called from both `listSessions` and `getSessionListItem`.
- R1-2/R1-3/R1-4/R1-7: tests for the legacy recovery arm, the
  prompt/title suppression guard at both producers, the `getSessionListItem`
  wiring, and the production file scan itself — the last one drives a real
  transcript whose goal record sits past the ten lines the records fallback
  parses, which is the only shape where a dead marker cannot hide behind it.
  The spy-based "prefers the latest objective" test is gone; it asserted the
  spy, not the code.
- R1-6: the other four label surfaces stay as they are; the asymmetry is now
  a documented decision on `SessionListItem.goalObjective` rather than an
  accident, with the shape a follow-up would take.

Mutation-verified: reverting the read to "last objective on any goal_state
line" reddens 3; letting the records fallback answer an out-of-window scan
reddens 1; a marker typo or a renamed field reddens 1 and 4; dropping the
suppression guard reddens 3; dropping the `getSessionListItem` wiring or the
legacy arm reddens 1 each.

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

* fix(core): harden Goal objective lifecycle reads

* fix(core): recover goal labels from complete records

* fix(core): harden Goal label recovery

* test(core): mock integrity reads in rename tests

* fix(core): reject nested goal fragments

* fix(core): reject ambiguous goal state recovery

* fix(core): close nested Goal recovery gaps

* fix(core): preserve no-follow session goal reads

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
…ounds (#10465)

* test(core): close the deferred test gaps recorded in #9930's review rounds

Three items #9930's rounds 3-4 recorded as non-blocking deferrals, now
resolved by the same author:

- Rotation fallback branch (config.test.ts): pinned via the re-claim
  scenario — the fallback holds a live Config reference, so only an
  interloper Config taking the fallback between construction and an
  un-contexted rotation makes the rotation-time claim observable.
  Deletion mutant now fails the test.
- Streak-cap recovery (debugLogger.test.ts): the cap must act as a
  circuit breaker, not a latch — a capped streak still attempts on a
  session change, and one success re-opens retries. The
  never-attempt-again latch mutant now fails the test.
- Drain-side sessionIdContext.exit (scheduler.ts): removed rather than
  tested — every path into the drain (start-side kick, .finally re-kick,
  retry timers) already runs context-free behind the start-side exit,
  so the second wrapper was unreachable defensive code no test could
  pin. The inner `return` becomes `continue` (equivalent: finally still
  runs, the while condition exits the loop).

Verification: core config 580/580 + debugLogger 41/41, cli scheduler
14/14 + acpAgent 531/531; both mutants killed by their named tests;
typecheck/ESLint/Prettier clean.

Relates to #9535, #9538, #9930.

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

* test(core): extract shared debug-fallback isolation helper (round-1 R1-1)

The two debug-fallback rotation tests copied ~28 lines of fs-spy + env
save/restore boilerplate verbatim, and both omitted a readlink spy — so
both already made a real readlink against the actual global debug dir, and
any future fs call added to the fallback/alias path would leak real writes
from whichever copy wasn't updated in lockstep. Extract
withDebugFallbackIsolation: it spies the full surface (mkdir/appendFile/
unlink/symlink/readlink) once, hands the body only the appendFile spy, and
restores env + logger state on exit. Behavior unchanged; mutation-verified
that the rotation-claim deletion still fails its test.

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

* test(core): fix TS2345 in withDebugFallbackIsolation (round-2 R1-1)

The helper typed its callback arg as ReturnType<typeof vi.spyOn>, which
resolves to vi.spyOn's generic-overload return type; the concrete
appendFile spy is not assignable to it, so tsc/build/CI failed with TS2345
at the call site (vitest transpiles without type-checking, so the tests
still ran green and the failure was invisible to a test-only run — I missed
it by not running typecheck on the previous commit). Drop the callback arg;
the two tests read the spy back via vi.mocked(fs.promises.appendFile), which
is correctly typed. appendFile folded into the spies array.

Verification: tsc --noEmit exit 0, config suite 580/580, rotation-claim
mutation still fails its test, ESLint + Prettier clean.

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

* docs(cli): point the drain's no-exit comment at the choke point (round-2 suggestion)

The invariant that makes the removed drain-side sessionIdContext.exit safe
lives at the start-side exit, not here. Spell that out and warn that a new
entry into the drain must go through the exited scope — makes the coupling
greppable, per the review suggestion. Comment-only.

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>
…#10522)

The failed-spawn compensating-write gate from #10223 compares
teamFileWritesStarted against the value captured at member push, but a
write that rejected (atomic temp+rename persisted nothing) still counts
as started. In the solo case the member's own write starts and throws
ENOSPC, the catch rolls the member back, and the gate reads 1 > 0 —
firing a compensating write that cannot repair anything. If the disk is
still full that write fails too, notifying the leader about a possible
ghost member that cannot exist on top of the spawn error that already
carries the real cause (#10297).

Decrementing the counter on rejection is unsafe: the counter is a
monotonic high-water mark, and a decrement lets a later write reuse the
value and hide below an earlier member's push watermark, re-introducing
the #10208 ghost.

Make the gate commit-aware instead:
- teamFileWritesStarted becomes the per-write sequence number, still
  assigned synchronously at the snapshot point and never decremented.
- A new teamFileWritesCommitted watermark records the sequence number
  of the most recently committed write; the queue is serial, so commits
  land in sequence order and the watermark stays monotonic.
- The compensating write is queued with onlyIfCommittedAfter set to the
  push-time watermark; the queued task runs after every earlier write
  (including any still in flight at gate time) has settled and writes
  only if a write above the watermark committed — a rejected window
  write persists nothing, so the redundant write is skipped.

Witness tests: the solo rejected-write case asserts exactly one write
attempt and no leader notice (red before the fix: the compensating
write fired), and the issue's five-step interleaving pins that a later
committed write still triggers the repair, guarding against a
decrement-on-reject regression.

Fixes #10297

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@pull pull Bot locked and limited conversation to collaborators Sep 1, 2026
@pull pull Bot added the ⤵️ pull label Sep 1, 2026
@pull
pull Bot merged commit 2b8f73c into bit-cook:main Sep 1, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants