[pull] main from QwenLM:main - #534
Merged
Merged
Conversation
* 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 : )