Skip to content

fix(cli): Preserve mid-turn image messages - #5183

Merged
wenshao merged 24 commits into
QwenLM:mainfrom
doudouOUC:fix/mid-turn-image-preservation
Jun 18, 2026
Merged

fix(cli): Preserve mid-turn image messages#5183
wenshao merged 24 commits into
QwenLM:mainfrom
doudouOUC:fix/mid-turn-image-preservation

Conversation

@doudouOUC

Copy link
Copy Markdown
Collaborator

What this PR does

This PR preserves mid-turn user image input across the CLI, ACP, and desktop Qwen backend paths. It lets mid-turn @ file messages resolve into structured parts before tool results are sent, extends the ACP mid-turn drain extension to carry structured content blocks, and forwards live desktop image attachments through the mid-turn queue instead of collapsing those messages to text.

Why it's needed

The root cause was that mid-turn messages were represented as plain strings in multiple handoff points. That worked for text, but it lost image parts from @ resolution and desktop attachments during tool execution. It also allowed the desktop session queue to acknowledge some image messages before safe replay when live image data was unavailable. This PR keeps the text-only behavior compatible while preserving structured image content and retaining unsafe image messages for the next turn.

Reviewer Test Plan

How to verify

Send a message while tools are still executing that references an image through @ in the CLI; the continuation request after the tool result should include both the prefixed user text and the image inline data. In desktop Qwen mode, send a queued mid-turn message with a live image attachment; the ACP drain should return structured content blocks with text and image data, and the queued message should be acknowledged by stable message id. Send a queued mid-turn message whose image attachment lacks live base64; it should remain queued for the next turn rather than being acknowledged and dropped.

Evidence (Before & After)

Before: mid-turn image messages were reduced to text or could be acknowledged before their image data was available, which made the image unavailable to the model. After: focused tests cover CLI @ image resolution during mid-turn tool continuation, ACP structured image block injection, desktop image attachment drain, and the missing-base64 fallback queue path.

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

Environment (optional)

macOS local worktree with Node/npm, Bun, Vitest, and GitHub CLI. Validation included focused CLI and desktop tests, TypeScript checks, and the repository build.

Risk & Scope

  • Main risk or tradeoff: The ACP mid-turn extension now accepts a structured items shape in addition to legacy messages, so reviewers should check clients that implement this extension continue to receive text-only legacy behavior when there are no attachments.
  • Not validated / out of scope: Full manual TUI reproduction and Windows/Linux local runs were not performed.
  • Breaking changes / migration notes: No breaking changes expected; the legacy messages response remains supported for text-only drains.

Linked Issues

References #5036.

中文说明

What this PR does

这个 PR 在 CLI、ACP 和 desktop Qwen backend 的 mid-turn 路径中保留用户图片输入。它会在工具结果发送前解析 mid-turn 的 @ 文件消息为结构化 parts,扩展 ACP mid-turn drain extension 以传递结构化 content blocks,并把 desktop 的 live 图片附件带入 mid-turn 队列,而不是把这些消息降级成纯文本。

Why it's needed

根因是多个 mid-turn 交接点都把消息表示成纯字符串。纯文本场景可以工作,但会丢失 @ 解析产生的图片 parts 和 desktop 附件。它还可能在 live 图片数据不可用时,让 desktop session queue 提前 ack 图片消息,导致后续无法安全 replay。这个 PR 保持纯文本行为兼容,同时保留结构化图片内容,并把不安全的图片消息留到下一轮处理。

Reviewer Test Plan

How to verify

在工具仍在执行时发送一条通过 CLI @ 引用图片的消息;工具结果后的 continuation request 应包含带前缀的用户文本和图片 inline data。在 desktop Qwen mode 中,发送一条带 live 图片附件的 mid-turn 队列消息;ACP drain 应返回包含文本和图片数据的结构化 content blocks,并通过稳定 message id ack 队列消息。再发送一条图片附件缺少 live base64 的 mid-turn 队列消息;它应保留在队列中等待下一轮,而不是被 ack 后丢弃。

Evidence (Before & After)

修复前:mid-turn 图片消息会被降级成文本,或者在图片数据可用前就被 ack,导致模型拿不到图片。修复后:聚焦测试覆盖了 CLI mid-turn 工具 continuation 中的 @ 图片解析、ACP 结构化图片 block 注入、desktop 图片附件 drain,以及缺少 base64 时回退保留队列的路径。

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

Environment (optional)

macOS 本地 worktree,使用 Node/npm、Bun、Vitest 和 GitHub CLI。验证包括 CLI 和 desktop 的聚焦测试、TypeScript 检查以及仓库构建。

Risk & Scope

  • Main risk or tradeoff: ACP mid-turn extension 现在除了 legacy messages 外也接受结构化 items shape,因此 review 时应确认实现该 extension 的 client 在无附件纯文本场景下仍保持 legacy 行为。
  • Not validated / out of scope: 未执行完整手动 TUI 复现,也未在 Windows/Linux 本地运行。
  • Breaking changes / migration notes: 预期没有 breaking changes;纯文本 drain 仍支持 legacy messages 响应。

Linked Issues

参考 #5036

Preserve image attachments and structured content when user messages are drained during tool execution. Keep text-only behavior compatible while preventing attachment-only or missing-base64 image messages from being acknowledged before they can be replayed safely.

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

@doudouOUC doudouOUC left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Downgraded from Request Changes to Comment: self-PR; CI failing: Lint.

3 Critical findings (2 ESLint errors blocking CI, 1 logic bug) and 3 Suggestions. — qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/ui/hooks/atCommandProcessor.ts Outdated
Comment thread packages/cli/src/acp-integration/session/Session.ts Outdated
Comment thread packages/cli/src/acp-integration/session/Session.ts
Comment thread packages/cli/src/acp-integration/session/Session.ts Outdated
Comment thread packages/cli/src/ui/hooks/useGeminiStream.ts Outdated
yiliang114 and others added 2 commits June 16, 2026 14:01
Fix the lint blockers, preserve legacy mid-turn messages when structured items are empty, share the mid-turn prefix helper, and record mid-turn @ command reads without adding UI tool groups.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC
doudouOUC marked this pull request as ready for review June 16, 2026 16:39
Copilot AI review requested due to automatic review settings June 16, 2026 16:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread packages/cli/src/acp-integration/session/Session.ts
Comment thread packages/cli/src/acp-integration/session/Session.ts
Comment thread packages/cli/src/acp-integration/session/Session.ts Outdated
Comment thread packages/cli/src/ui/hooks/useGeminiStream.ts Outdated
Comment thread packages/desktop/packages/shared/src/agent/qwen-agent.ts Outdated
Comment thread packages/desktop/packages/shared/src/agent/qwen-agent.ts Outdated
doudouOUC and others added 2 commits June 17, 2026 01:57
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
qqqys
qqqys previously approved these changes Jun 16, 2026

@qqqys qqqys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical re-check: the mid-turn image/message drain issues raised earlier are fixed at HEAD, and I found no remaining critical blockers in this pass.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review found no critical issues. 3 suggestions below. Downgraded from Approve to Comment: CI still running. — qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/acp-integration/session/Session.ts Outdated
Comment thread packages/cli/src/utils/midTurnUserMessage.ts Outdated
Comment thread packages/cli/src/acp-integration/session/Session.ts
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully: Qwen review timed out after 85 minutes. See workflow logs.

qqqys
qqqys previously approved these changes Jun 16, 2026

@qqqys qqqys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical re-check after the latest mid-turn hardening commit: the previously reviewed image/message drain issues remain fixed, and I found no new critical blocker in the incremental diff.

Comment thread packages/cli/src/ui/hooks/useGeminiStream.ts
Comment thread packages/cli/src/ui/hooks/useGeminiStream.ts Outdated
Bound mid-turn @ resolution with a per-message timeout and surfaced attachment failures to the user before falling back to raw text.

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

@doudouOUC doudouOUC left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Downgraded from Request Changes to Comment: self-PR.

Re-review of latest commit. Previous Critical findings (batch error handling, orphan AbortController) are resolved — the per-message try/catch and withTimeoutSignal(abortSignal, ...) correctly address both concerns. Remaining findings below are Suggestions.

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/ui/hooks/useGeminiStream.ts
Comment thread packages/cli/src/acp-integration/session/Session.ts
Comment thread packages/cli/src/acp-integration/session/Session.test.ts Outdated
Comment thread packages/cli/src/utils/midTurnUserMessage.ts

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One additional finding on the Gemini drain loop (supplementing the 12 comments already posted). — Qwen Code /review

Comment thread packages/cli/src/ui/hooks/useGeminiStream.ts
doudouOUC and others added 3 commits June 17, 2026 09:18
Surface mid-turn at-command tool displays, validate ACP inline media MIME prefixes, and add focused coverage for mid-turn message prefixing.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Stop processing drained mid-turn messages once cancellation aborts at-command resolution, avoiding stale fallback notifications and recordings.

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

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Review — PR #5183 (fix(cli): Preserve mid-turn image messages)

All 297 tests pass (CLI + Desktop). TypeScript and lint checks clean.

This PR extends the ACP mid-turn drain to carry structured content blocks (text, image, audio, resource) alongside the legacy text-only messages format, and lets the desktop client forward image attachments mid-turn. The overall design is sound — the structured items path cleanly separates from the legacy messages path, the abort/timeout plumbing is careful, and the resolveAtCommandQuery extraction is a clean refactor.

I found 1 critical issue and 3 suggestions. Inline comments below.

Comment thread packages/cli/src/ui/hooks/useGeminiStream.ts
Comment thread packages/desktop/packages/shared/src/agent/qwen-agent.ts Outdated
Comment thread packages/cli/src/acp-integration/session/Session.ts Outdated
Comment thread packages/cli/src/acp-integration/session/Session.ts
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Comment thread packages/cli/src/ui/hooks/useGeminiStream.ts
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary (Cross-repo lightweight mode)

This PR extends the mid-turn message pipeline to carry structured image/attachment content through CLI ACP, useGeminiStream, atCommandProcessor, and desktop paths. The overall design is sound — the separation between text-only (messages) and structured (items) drain responses, the canOfferMidTurnAttachments gate, and the isContentBlock validator all work together coherently.

The existing review comments from @doudouOUC and the CI bot cover the substantive issues thoroughly. Below is one additional finding from an independent analysis pass.

Reviewed by: qwen3.7-max

Comment thread packages/desktop/packages/shared/src/agent/qwen-agent.ts Outdated
doudouOUC and others added 2 commits June 17, 2026 21:18
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@wenshao

wenshao commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Maintainer local verification — real test build under tmux (red/green + typecheck)

I built a worktree at the PR head and ran the actual test suites under tmux on Linux (the PR table marks Linux not tested), then proved the new tests guard the fix via red/green, and typechecked the touched packages.

Verdict: verified locally. All changed suites pass on all three surfaces; the new tests genuinely guard the fix (clean red/green); types are clean; there are no merge conflicts. The only hard blocker is pending CI. The single open review thread is a non-blocking performance suggestion.

Environment

  • Worktree at pull/5183/head = ba634c674 (git rev-parse confirmed it matches the PR head).
  • npm ci in the worktree → clean (0 npm errors). CLI suites run with vitest.
  • packages/desktop is excluded from the root workspace ("!packages/desktop") and uses Bun, so a separate bun install (1554 pkgs) + bun test.
  • Red/green baseline = merge-base with origin/main = 3ff7e7f95 ("Stop after cancelled ask_user_question fix(cli): Stop after cancelled ask_user_question #5218"); git diff base..head is exactly the 12 PR files.
  • OS: Linux · Node 22.22 · Bun 1.3.14.

1) Changed suites — GREEN on PR code

Surface Suite Result
CLI core utils/midTurnUserMessage.test.ts 3/3
CLI UI ui/hooks/useGeminiStream.test.tsx 118/118
CLI ACP acp-integration/session/Session.test.ts 150/150
Desktop qwen-native-history.test.ts + qwen-agent-slash-history.test.ts 62/62 ✅ (232 assertions)

CLI total 271 passed; desktop 62 passed / 0 failed.

2) Red/green — the new tests actually guard the fix

Reverted only the impl files to baseline 3ff7e7f95 (deleting the new midTurnUserMessage.ts), kept the PR's test files, and re-ran:

  • CLI: exactly 11 PR-new tests fail + midTurnUserMessage.test.ts fails to import — the other 257 pre-existing tests still pass.
  • Desktop: exactly 10 PR-new tests fail — the other 52 still pass.

So the failures map 1:1 to the PR's new behavior (mid-turn @/image resolution, structured-items drain, content-block validation, ack-by-messageId, live-image offering), including the security-relevant "rejects mid-turn resource links…" case.

Exact failing tests on the pre-fix baseline (21 total)

CLI — useGeminiStream.test.tsx (6):

  • records mid-turn queued user messages before submitting tool results
  • resolves mid-turn @ image messages before submitting tool results
  • skips mid-turn @ injection when resolution should not proceed
  • warns and skips mid-turn @ injection when resolution fails
  • times out stalled mid-turn @ resolution before submitting tool results
  • skips mid-turn @ fallback side effects when cancelled during resolution

CLI — Session.test.ts (5):

  • injects drained structured mid-turn user messages with images
  • keeps later structured mid-turn messages when one resolution fails
  • stops draining mid-turn messages when structured resolution is aborted
  • logs unrecognized mid-turn drain response fields
  • rejects mid-turn resource links and keeps valid messages in the same batch

CLI — midTurnUserMessage.test.ts: whole file fails to import (impl deleted at baseline).

Desktop — qwen-agent-slash-history.test.ts (5):

  • drains queued mid-turn messages through the ACP extension handler
  • drains queued mid-turn image attachments as ACP content blocks
  • drains mixed text and image mid-turn messages as ACP items
  • acknowledges image-only mid-turn messages by optimistic id
  • uses unique fallback ids for metadata-free image-only mid-turn messages

Desktop — qwen-native-history.test.ts (5):

  • offers live image attachments to Qwen mid-turn injection
  • offers plain text follow-ups to Qwen mid-turn injection after visual messages
  • acknowledges Qwen mid-turn queued messages by messageId
  • does not text-match identified Qwen mid-turn queued messages
  • warns when Qwen mid-turn drain acknowledgements do not match

3) Typecheck — clean

tsc --noEmit: CLI ✅ 0 · desktop shared ✅ 0 · desktop server-core ✅ 0.

4) Cross-surface interop + security (code-read & test-confirmed)

  • The desktop qwen-agent.ts drain emits { items: [{ content: ContentBlock[], displayText }] } — exactly the shape the ACP Session.ts parseMidTurnDrainResponse consumes. Producer side is covered by "drains queued mid-turn image attachments as ACP content blocks", consumer side by "injects drained structured mid-turn user messages with images".
  • Content-block validation rejects resource_link (blocks file:///…-style mid-turn injection) and enforces image/* / audio/* mime prefixes (rejects text/html disguised as an image). Both confirmed by the red/green delta.
  • Mid-turn resolution is abort-bounded (10s timeout + parent signal) and sequential by design — see the open-thread note below.

5) Merge readiness

  • mergeable: MERGEABLEno conflicts with origin/main.
  • mergeStateStatus: BLOCKED is from pending CI (CodeQL / Lint / Test×3), not conflicts.
  • The CHANGES_REQUESTED (qwen-code-ci-bot, 11:12Z) predates the current head (13:42Z); the later bot review (13:03Z) only COMMENTED.
  • 1 unresolved review thread (Session.ts:2102) — a performance suggestion to parallelize #resolvePrompt with Promise.allSettled. Worth noting this is a deliberate tradeoff: the sequential loop + early-return-on-abort is exactly what makes the "stops draining when aborted" guarantee hold (parallelizing would start all N resolutions before an abort can stop the tail). Reasonable to defer as an optional follow-up; not a correctness blocker.

Scope / caveats

  • Desktop tests need CRAFT_CONFIG_DIR pointed at a dir seeded with config-defaults.json (the SessionManager constructor calls loadConfigDefaults()); without it the constructor throws. Environmental — affects any branch, not a PR regression. (An unseeded run shows 30 such failures; after seeding, 62/62 pass.)
  • I did not stage a full manual TUI/daemon reproduction of a message arriving during live tool execution (timing-sensitive). The integration suites drive the real Session / SessionManager / QwenAgent / useGeminiStream code with mocked I/O boundaries, and red/green confirms they exercise the new paths.

Net: ready to merge once CI is green; the single open thread is an optional perf follow-up.

🇨🇳 中文版(点击展开)

维护者本地验证 — tmux 下的真实测试构建(red/green + 类型检查)

我在 PR head 上建了 worktree,在 Linux 下用 tmux 跑了真实的测试套件(PR 表格里 Linux 标的是 not tested),然后用 red/green 证明新测试确实守护了修复,并对改动的包做了类型检查。

结论:本地验证通过。 三个改动面(CLI @、ACP Session、desktop Qwen backend)的所有改动套件全绿;新测试确实守护了修复(red/green 干净);类型检查通过;与 main 无合并冲突。唯一的硬阻塞是 CI 还在 pending。仅剩的一个未解决 review thread 是非阻塞的性能建议。

环境

  • worktree 在 pull/5183/head = ba634c674git rev-parse 确认与 PR head 一致)。
  • worktree 内 npm ci → 干净(0 npm error)。CLI 套件用 vitest
  • packages/desktop 被根 workspace 排除("!packages/desktop")且用 Bun,所以单独 bun install(1554 包)+ bun test
  • red/green 基线 = 与 origin/main 的 merge-base = 3ff7e7f95git diff base..head 恰好就是 PR 的 12 个文件。
  • 系统:Linux · Node 22.22 · Bun 1.3.14。

1) 改动套件 — PR 代码上全绿

套件 结果
CLI core utils/midTurnUserMessage.test.ts 3/3
CLI UI ui/hooks/useGeminiStream.test.tsx 118/118
CLI ACP acp-integration/session/Session.test.ts 150/150
Desktop qwen-native-history.test.ts + qwen-agent-slash-history.test.ts 62/62 ✅(232 断言)

CLI 合计 271 通过;desktop 62 通过 / 0 失败

2) Red/green — 新测试确实守护修复

只把 实现 文件回退到基线 3ff7e7f95(删掉新增的 midTurnUserMessage.ts),保留 PR 的 测试 文件,重跑:

  • CLI: 恰好 11 个 PR 新增测试失败 + midTurnUserMessage.test.ts import 失败 —— 其余 257 个原有测试仍通过。
  • Desktop: 恰好 10 个 PR 新增测试失败 —— 其余 52 个仍通过。

失败项与 PR 新增行为一一对应(mid-turn @/图片解析、结构化 items drain、content-block 校验、按 messageId ack、live 图片下发),包括安全相关的 "rejects mid-turn resource links…"。失败测试清单见上方英文折叠块。

3) 类型检查 — 干净

tsc --noEmit:CLI ✅ 0 · desktop shared ✅ 0 · desktop server-core ✅ 0。

4) 跨面互通 + 安全(代码阅读 + 测试确认)

  • desktop qwen-agent.ts 的 drain 产出 { items: [{ content: ContentBlock[], displayText }] },正是 ACP Session.tsparseMidTurnDrainResponse 消费的形状。生产端由 "drains queued mid-turn image attachments as ACP content blocks" 覆盖,消费端由 "injects drained structured mid-turn user messages with images" 覆盖。
  • content-block 校验拒绝 resource_link(挡住 file:///… 式的 mid-turn 注入),并强制 image/* / audio/* mime 前缀(拒绝伪装成图片的 text/html)。两者都由 red/green 差异确认。
  • mid-turn 解析有 abort 上界(10s 超时 + 父信号)且刻意串行 —— 见下方未解决 thread 说明。

5) 可合并性

  • mergeable: MERGEABLE —— 与 origin/main 无冲突
  • mergeStateStatus: BLOCKED 来自 CI pending(CodeQL / Lint / Test×3),不是冲突。
  • CHANGES_REQUESTED(qwen-code-ci-bot,11:12Z)早于当前 head(13:42Z);之后 13:03Z 的 bot review 只是 COMMENTED
  • 1 个未解决 review threadSession.ts:2102)—— 性能建议:用 Promise.allSettled#resolvePrompt 并行化。值得说明这是个刻意权衡:串行循环 + abort 时提前返回,正是 "stops draining when aborted" 这条保证成立的原因(并行会在 abort 能停下后续之前就启动全部 N 个解析)。作为可选后续优化合理,不是正确性阻塞。

范围 / 说明

  • desktop 测试需要把 CRAFT_CONFIG_DIR 指向一个预置了 config-defaults.json 的目录(SessionManager 构造函数会调 loadConfigDefaults()),否则构造函数抛错。这是环境问题,对任何分支都一样,不是 PR 回归。(未预置时会有 30 个这类失败;预置后 62/62 通过。)
  • 我没有手工搭建"工具执行过程中消息到达"的完整 TUI/daemon 复现(时序敏感)。集成套件已经驱动真实的 Session / SessionManager / QwenAgent / useGeminiStream 代码(仅 mock I/O 边界),red/green 也确认它们走到了新路径。

结论:CI 转绿后即可合并;唯一未解决 thread 是可选的性能后续项。

Local verification on Linux (worktree at ba634c674); CI remains the authoritative gate.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

Overall this PR makes a solid improvement — the structured content block pipeline for mid-turn images is well-architected and the separation of resolveAtCommandQuery (pure) from handleAtCommand (side effects) is a good refactor.

One inline suggestion noted regarding the randomUUID() fallback in the drain handler.

Comment thread packages/desktop/packages/shared/src/agent/qwen-agent.ts Outdated
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Comment thread packages/desktop/packages/shared/src/agent/qwen-agent.ts Outdated
Comment thread packages/cli/src/acp-integration/session/Session.ts
Comment thread packages/cli/src/acp-integration/session/Session.ts
Comment thread packages/cli/src/acp-integration/session/Session.ts
return text.replace(/^(?:[ \t]*\r?\n)+/, '');
}

async function resolveWithAbort<T>(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] resolveWithAbort (here) and withTimeoutSignal (in Session.ts) are near-identical abort-race utilities — both implement the same listener-setup, Promise.race, cleanup-in-finally pattern. They differ only in that withTimeoutSignal adds AbortSignal.any with a timeout layer.

Two independently maintained copies of the same logic. A correctness fix or optimization in one must be replicated to the other. Consider extracting a shared utility into packages/cli/src/utils/.

— qwen3.7-max via Qwen Code /review

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

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

3 new findings not covered by existing review comments. Build passes, 272 tests pass.

Key concern: The desktop mid-turn queue (qwen-agent.ts) lacks a retry counter for failed entries, unlike the Session.ts ACP path which has MID_TURN_QUEUE_DRAIN_MAX_TIMEOUT_STRIKES. A deterministic buildPromptBlocks failure causes an infinite retry loop.

Also: isEmbeddedResourceResource accepts unbounded text (potential OOM), and recordMidTurnUserMessage now persists base64 image data in chat recording files.

Comment thread packages/desktop/packages/shared/src/agent/qwen-agent.ts
Comment thread packages/cli/src/acp-integration/session/Session.ts
?.recordMidTurnUserMessage([part], message);
return part;
});
?.recordMidTurnUserMessage(parts, displayText);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Nice to have] recordMidTurnUserMessage(parts, displayText) now receives Part[] containing inlineData with raw base64 image content. The chat recording service writes this to session JSONL, which could significantly inflate recording file sizes (base64 adds ~33% overhead over raw binary) and embed binary blobs in the session history file.

Consider whether the recording service should strip inlineData from persisted parts (replacing with a placeholder or reference) to keep recording files manageable.

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

wenshao commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

🔬 Maintainer verification — live during-tool-execution TUI reproduction (head ba634c674)

This closes the one gap my earlier verification on this same head left open:

"I did not stage a full manual TUI/daemon reproduction of a message arriving during live tool execution (timing-sensitive)."

I drove the real built CLI (dist/cli.js) under tmux with a scripted mock model, queued a mid-turn @image message while a tool was actually executing, and captured the continuation request that goes to the model. The image is preserved end-to-end, byte-for-byte. (Tests/typecheck/red-green were already covered above and still pass on this head — re-confirmed below.)

Harness

  • Linux · Node 22 · PR head ba634c674 (dist/cli.js rebuilt via npm run build && npm run bundle).
  • Real 16×16 PNG at /tmp/mt5183/pic.png. Isolated HOME, OpenAI-compatible mock server that logs every request body.
  • Turn 1 → the mock returns a run_shell_command tool call running sleep 30 (allowed via # intentional-sleep:), creating a real ~30s mid-turn window. Only the tool call is scripted; the confirmation flow, shell execution, @ resolution, mid-turn drain, and request encoding are all the real code path. --approval-mode=yolo so the tool auto-runs.

What happened (live)

  1. Tool starts: ✓ Shell sleep 30 … [in /tmp/mt5183] (turn active).
  2. While it runs I submit @pic.png what is shown in this image. The TUI shows it queuedPress ↑ to edit queued messages.
  3. sleep finishes → the continuation (tool result) request fires. Only two requests total were sent, so the image rode in the same tool-result continuation — i.e. genuinely mid-turn, not a follow-up turn.

The captured continuation request (req #2) — image preserved

Message roles: system · user · assistant · tool · user. The trailing user message (drained mid-turn alongside the tool result) contains:

text:      "\n[User message received during tool execution]: @pic.png what is shown…"
text:      "\n--- Content from referenced files ---"
text:      "\nContent from /tmp/mt5183/pic.png:\n"
image_url: "data:image/png;base64,iVBORw0KGgoAAAANSU…"   ← real image inline data
text:      "\n--- End of content ---"

Verified programmatically: the image_url declares data:image/png;base64,, and its base64 is byte-for-byte identical to the on-disk pic.png. The mid-turn prefix is present, and the @ reference resolved to actual inline image data — exactly what the PR promises ("the continuation request after the tool result should include both the prefixed user text and the image inline data").

Before (merge-base 3ff7e7f95, the removed code — authoritative):

for (const msg of drained) {
  const midTurnUserMessage = { text: `\n[User message received during tool execution]: ${msg}` };
  responsesToSend.push(midTurnUserMessage);  // string-collapsed: no @ resolution, no image
}

So pre-fix, the same mid-turn @pic.png would have reached the model as the literal text @pic.png what is shown in this image with no image. The diff between that and the live capture above is the fix, working.

⚠️ One scoping nuance worth a line in the PR (not a bug)

The image is only preserved when the configured model is vision-capable. I first ran with a non-vision model name and the same @pic.png was replaced upstream of the mid-turn logic (at @/read resolution, per modalityDefaults.ts) with a text placeholder:

[Unsupported image file: "pic.png". This model does not support image input. The read_file …]

This is correct behavior — you wouldn't send image bytes to a text-only model — but it means the fix's benefit is scoped to vision-capable models, and a reviewer testing this must point the session at one (e.g. a qwen-vl-* / coder-model / gpt-*-class model). The text-only path degrades gracefully to the placeholder.

Re-confirmed on ba634c674 (so this report stands alone)

  • Changed suites GREEN: midTurnUserMessage 3/3, useGeminiStream 118/118, ACP Session 150/150, plus atCommandProcessor 43/43 (the refactor of handleAtCommandresolveAtCommandQuery leaves the normal @ path intact); desktop qwen-native-history 30/30 + qwen-agent-slash-history 32/32. 376 passing.
  • tsc --noEmit clean: CLI + desktop shared + desktop server-core.
  • Desktop suites need CRAFT_CONFIG_DIR seeded with config-defaults.json (the SessionManager constructor calls loadConfigDefaults()); environmental, affects any branch — once seeded, 62/62 pass.

Verdict

The runtime behavior the unit tests model is now confirmed in a real TUI: a mid-turn @image typed during live tool execution reaches the model as actual inline image data, in the same tool-result continuation, byte-exact — and pre-fix it would have been dropped to text. No correctness issues found. This strengthens the earlier "ready to merge once CI is green" with live end-to-end evidence; the only open thread remains the optional Promise.allSettled perf suggestion.

🇨🇳 中文版(点击展开)

🔬 维护者验证 —— 在工具真实执行期间的实时 TUI 复现(head ba634c674

这补上了我此前在同一 head 上验证时留下的唯一缺口:

“我没有做一个完整的手动 TUI/daemon 复现:在工具真实执行期间到达的消息(时序敏感)。”

我用脚本化 mock 模型在 tmux 下驱动真实构建的 CLIdist/cli.js),在一个工具正在执行时排入一条 mid-turn 的 @image 消息,并抓取了发往模型的 continuation request。图片被端到端、逐字节保留。(测试/类型检查/red-green 上面已覆盖,本 head 仍全部通过,下方再次确认。)

测试装置

  • Linux · Node 22 · PR head ba634c674npm run build && npm run bundle 重建 dist/cli.js)。
  • /tmp/mt5183/pic.png 放一张真实的 16×16 PNG。隔离 HOME,OpenAI 兼容的 mock server 记录每个请求体。
  • 第 1 轮 → mock 返回一个 run_shell_command 工具调用执行 sleep 30(用 # intentional-sleep: 放行),制造约 30s 的真实 mid-turn 窗口。只有工具调用是脚本化的;确认流程、shell 执行、@ 解析、mid-turn drain、请求编码都是真实代码路径。用 --approval-mode=yolo 让工具自动执行。

实时过程

  1. 工具开始:✓ Shell sleep 30 … [in /tmp/mt5183](turn 处于活动状态)。
  2. 在它运行期间我提交 @pic.png what is shown in this image。TUI 显示它已排队——Press ↑ to edit queued messages
  3. sleep 结束 → continuation(工具结果)请求发出。总共只发了两个请求,所以图片是搭着同一个工具结果 continuation 一起发出的——也就是真正的 mid-turn,而不是后续的新一轮。

抓到的 continuation request(第 2 个)—— 图片被保留

消息角色:system · user · assistant · tool · user。最后那条 user 消息(与工具结果一起 mid-turn drain 出来)包含:

text:      "\n[User message received during tool execution]: @pic.png what is shown…"
text:      "\n--- Content from referenced files ---"
text:      "\nContent from /tmp/mt5183/pic.png:\n"
image_url: "data:image/png;base64,iVBORw0KGgoAAAANSU…"   ← 真实图片 inline data
text:      "\n--- End of content ---"

程序化校验:image_urldata:image/png;base64, 声明,其 base64 与磁盘上的 pic.png 逐字节一致。mid-turn 前缀存在,@ 引用解析成了真实的 inline 图片数据——正是 PR 所承诺的(“工具结果之后的 continuation request 应同时包含带前缀的用户文本与图片 inline data”)。

修复前(merge-base 3ff7e7f95,被删掉的代码——权威依据):

for (const msg of drained) {
  const midTurnUserMessage = { text: `\n[User message received during tool execution]: ${msg}` };
  responsesToSend.push(midTurnUserMessage);  // 降级为字符串:无 @ 解析,无图片
}

所以修复前,同样的 mid-turn @pic.png 到达模型时只是字面文本 @pic.png what is shown in this image没有图片。上面实时抓取与这段旧代码之间的差异,就是这个修复在起作用。

⚠️ 一个值得在 PR 里写一句的范围说明(不是 bug)

只有当配置的模型支持视觉时,图片才会被保留。我一开始用一个非视觉模型名跑,结果同样的 @pic.pngmid-turn 逻辑之前(在 @/read 解析处,依据 modalityDefaults.ts)就被替换成了文本占位符:

[Unsupported image file: "pic.png". This model does not support image input. The read_file …]

这是正确行为——不会把图片字节发给纯文本模型——但这意味着该修复的收益仅限于视觉模型,复现时需要把会话指向一个视觉模型(如 qwen-vl-* / coder-model / gpt-* 类)。纯文本路径会优雅降级为占位符。

ba634c674 上再次确认(使本报告可独立成立)

  • 改动的测试套件全绿:midTurnUserMessage 3/3useGeminiStream 118/118、ACP Session 150/150,外加 atCommandProcessor 43/43handleAtCommandresolveAtCommandQuery 的重构未破坏常规 @ 路径);desktop qwen-native-history 30/30 + qwen-agent-slash-history 32/32共 376 通过。
  • tsc --noEmit 干净:CLI + desktop shared + desktop server-core
  • desktop 套件需要把 CRAFT_CONFIG_DIR 指向一个已放置 config-defaults.json 的目录(SessionManager 构造函数会调用 loadConfigDefaults());这是环境问题,对任何分支都一样——seed 之后 62/62 通过。

结论

单元测试所建模的运行时行为,现已在真实 TUI 中得到确认:在工具真实执行期间输入的 mid-turn @image,会作为真实的 inline 图片数据、随同一个工具结果 continuation、逐字节地到达模型——而修复前它会被降级成文本。未发现正确性问题。这以实时端到端证据强化了此前“CI 绿了即可合并”的结论;唯一未决线程仍是那个可选的 Promise.allSettled 性能建议。

Comment thread packages/cli/src/acp-integration/session/Session.ts
Comment thread packages/cli/src/acp-integration/session/Session.ts
attachments?: FileAttachment[],
metadata?: MidTurnMessageMetadata,
): boolean {
const trimmed = message.trim();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] enqueueMidTurnMessage pushes to midTurnMessageQueue with no cap on queue length or total base64 payload size. Before this PR, the queue was string[] (text only, a few KB per entry) and SessionManager blocked mid-turn messages carrying attachments. Now the queue carries QueuedMidTurnMessage[] with FileAttachment[] containing base64-encoded images (potentially MBs each). During a long tool execution with no drain boundary, a user could enqueue many large images, all held in memory.

Consider adding a cap:

Suggested change
const trimmed = message.trim();
const MAX_QUEUE_BASE64_BYTES = 50 * 1024 * 1024;
const currentSize = this.midTurnMessageQueue.reduce(
(sum, e) => sum + (e.attachments?.reduce((s, a) => s + (a.base64?.length ?? 0), 0) ?? 0),
0,
);
const incomingSize = attachments?.reduce((s, a) => s + (a.base64?.length ?? 0), 0) ?? 0;
if (currentSize + incomingSize > MAX_QUEUE_BASE64_BYTES) return false;
const trimmed = message.trim();

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/acp-integration/session/Session.ts
resolvedMidTurnQuery,
msg,
);
const formatCheck = checkImageFormatsSupport(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] checkImageFormatsSupport runs on mid-turn @ command results, but no test in useGeminiStream.test.tsx exercises this path. The existing mid-turn @ test uses image/png (a supported format). A grep for checkImageFormats, hasUnsupportedFormats, and unsupported.*format in the test file returns zero matches.

If checkImageFormatsSupport has a bug in how it inspects Part objects after prefixMidTurnUserMessageParts wraps them, users would get no format-warning notification for mid-turn image attachments even though the pre-submit path (line 1861) correctly warns.

— qwen3.7-max via Qwen Code /review

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No new high-confidence findings after multi-agent review (9 agents + reverse audit). 3 low-confidence items noted for human review (see terminal output). Downgraded from Approve to Comment: CI still running. — qwen3.7-max via Qwen Code /review

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

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Downgraded from Approve to Comment: CI still running.

5 new suggestions (supplementing the 28 inline comments already posted). Build passes, 274 CLI tests pass. — qwen3.7-max via Qwen Code /review

const MID_TURN_QUEUE_RESOLVE_TIMEOUT_MS = 10_000;
const MAX_MID_TURN_DRAIN_ITEMS = 10;
const MID_TURN_ATTACHMENT_PROCESSING_FAILURE_TEXT =
'[Attachment could not be processed]';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] MID_TURN_ATTACHMENT_PROCESSING_FAILURE_TEXT is defined here with the identical value '[Attachment could not be processed]' as in packages/desktop/packages/shared/src/agent/qwen-agent.ts:107. The MID_TURN_USER_MESSAGE_PREFIX duplication was already resolved by extracting it into midTurnUserMessage.ts, but this second user-facing string constant was not included in that extraction.

If the failure message needs to change (e.g., for localization or UX polish), two sites must be updated in lockstep. A mismatch produces inconsistent user-facing messages between the CLI ACP path and the desktop path.

Consider extracting to a shared constants module, or at minimum add a // SYNC: also defined in qwen-agent.ts comment at both sites.

— qwen3.7-max via Qwen Code /review

];
if (
message.kind === 'structured' &&
hasInlineMediaContentBlock(message.content)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] No test verifies the text-only structured message failure path. The existing failure tests ('keeps later structured mid-turn messages when one resolution fails' and 'adds a fallback marker when audio resolution fails') use items with image or audio content blocks, so hasInlineMediaContentBlock always returns true in failure paths.

The branch where it returns false (text-only structured message fails) is untested. A future refactor could accidentally remove the hasInlineMediaContentBlock guard, causing text-only messages to receive a misleading [Attachment could not be processed] marker on resolution failure.

Add a test case where a structured item has only { type: 'text', text: '...' } content blocks and #resolvePrompt throws. Assert the resulting parts contain the prefix + displayText fallback but do NOT contain the failure marker.

— qwen3.7-max via Qwen Code /review

Comment thread packages/desktop/packages/shared/src/agent/qwen-agent.ts
Comment thread packages/desktop/packages/shared/src/agent/qwen-agent.ts
Comment thread packages/cli/src/ui/hooks/useGeminiStream.ts Outdated
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

@doudouOUC doudouOUC left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Downgraded from Approve to Comment: self-PR.

Re-review with qwen3.7-max. Build passes, all 240 CLI tests pass. No new high-confidence issues found beyond the 45+ inline comments from prior rounds.

5 low-confidence findings (needs human review, not posted as inline comments):

  1. Queue splice precedes ack callback — narrow data-loss window if callback throws
  2. messageIds parameter can contain raw text/empty strings (defensive fallback paths)
  3. Sequential resolution with no aggregate timeout (200s worst case, unlikely scenario)
  4. Desktop ack mismatch could cause double delivery (requires unusual ID mismatch)
  5. Test coverage gaps: 7 untested edge-case branches (unsupported image format, resource block, mixed attachments, etc.)

All prior Critical issues have been addressed. PR looks ready to merge.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR, @doudouOUC!

Template looks good ✓ — every required heading is present, the reviewer test plan is concrete (how-to-verify, before/after, per-OS matrix, environment, risks), and the bilingual block is complete.

Direction. Mid-turn user messages being collapsed to plain strings is a real bug with a user-visible cost: @-referenced images and desktop attachments get silently dropped before the model sees them, which is exactly the class of loss the linked issue #5036 describes. This is squarely inside qwen-code's core mission (reliable multi-turn, multi-modal tool use) — not a "can-do" tangent. The CHANGELOG already records a cluster of related mid-turn work (#5175 deliver web-shell mid-turn messages, #4925 drain hang fix, #4580 rewind false-positive when mid-turn messages exist), which puts this fix on a well-trodden path rather than a new direction. ✅ aligned.

Scope. The PR does three coupled things and all three are needed to actually fix the bug end-to-end:

  1. A new prefixMidTurnUserMessageParts helper so the CLI can carry image Parts (not just text) through the mid-turn prefix.
  2. An additive extension of the ACP mid-turn drain shape: { items: [{ content: ContentBlock[], displayText }] } alongside the legacy { messages: string[] }, with strict content-block validation (rejects resource_link — closes the file:/// mid-turn injection vector — and enforces image/* / audio/* mime prefixes).
  3. Desktop qwen-agent plumbing to forward live image attachments through the queue and ack by stable message id, with a fallback that re-queues entries whose attachments aren't yet buildable rather than dropping them.

Cutting any one of those leaves the bug half-fixed on one surface. The ratio is test-heavy (~2500 of the +3350 lines are tests, and red/green from the maintainer's Linux verification confirms they actually guard the new behavior). No drive-by refactors, no scope creep past the stated goal.

Two notes, neither blocking:

  • The commit history is 20 commits long with several codex: address PR review feedback (#5183) and fix(cli): … mid-turn … fixups stacked on top of each other. Worth a squash at merge time to keep git log clean — but that's a merge-button setting, not a review concern.
  • The one unresolved review thread on Session.ts:2102 (Promise.allSettled parallelism suggestion) is, as the maintainer's own verification already noted, a deliberate tradeoff: sequential resolution + early abort is what makes "stops draining when aborted" hold. Deferring that to an optional follow-up is the right call.

Moving on to code review. 🔍

中文说明

感谢 @doudouOUC 的贡献!

模板完整 ✓ —— 所有必填标题齐全,reviewer 测试方案具体(如何复现、修复前后对比、按 OS 矩阵、环境、风险),双语折叠块完整。

方向。mid-turn 用户消息被降级为纯字符串,是个真实、用户可感的 bug:@ 引用的图片和 desktop 附件在模型看到之前就被悄悄丢掉了,这正是关联 issue #5036 描述的损失类型。它完全落在 qwen-code 的核心使命内(可靠的多轮、多模态工具使用),不是"能做就做"的偏题。CHANGELOG 里已有一组 mid-turn 相关工作(#5175 web-shell mid-turn 投递、#4925 drain 挂起修复、#4580 mid-turn 消息存在时的 rewind 误报),说明这条路径已被反复打磨,而非新开辟的方向。✅ 对齐。

范围。PR 做了三件彼此耦合的事,而三件都是真正把 bug 端到端修掉所必需的:

  1. 新增 prefixMidTurnUserMessageParts 辅助,让 CLI 在 mid-turn 前缀里携带图片 Part(不只是文本)。
  2. ACP mid-turn drain 形状的加法式扩展:{ items: [{ content: ContentBlock[], displayText }] } 与 legacy { messages: string[] } 并存,并对 content block 做严格校验(拒绝 resource_link —— 堵住 file:/// 式的 mid-turn 注入向量;强制 image/* / audio/* mime 前缀)。
  3. desktop qwen-agent 把 live 图片附件通过队列转发,并按稳定 message id ack;对附件暂时还构建不出来的条目,回退是重新入队而非丢弃。

砍掉其中任何一件,都会在某个面上留下"修了一半"的状态。测试占比高(+3350 行里约 2500 行是测试),而且维护者在 Linux 上的 red/green 验证确认这些测试真的守护了新行为。没有顺手重构,也没有超出既定目标的范围蔓延。

两条非阻塞备注:

  • 提交历史有 20 个 commit,里面有几个 codex: address PR review feedback (#5183)fix(cli): … mid-turn … 的 fixup 叠加。合并时建议 squash 一下,保持 git log 干净 —— 但这是合并按钮的设置,不是 review 问题。
  • Session.ts:2102 那条未解决的 review thread(建议用 Promise.allSettled 并行化)如维护者自己验证里所说,是刻意权衡:串行解析 + abort 时提前返回,正是"abort 时停止 drain"这条保证成立的原因。作为可选的后续优化是合理的。

进入代码审查 🔍

Qwen Code · qwen3.7-max


const messages = this.midTurnMessageQueue.splice(0);
if (messages.length > 0) {
const entries = this.midTurnMessageQueue.splice(0);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] Silent message loss when >10 mid-turn messages are queued. splice(0) drains ALL entries from midTurnMessageQueue and onMidTurnMessagesDrained(messageIds) acknowledges ALL of them. But on the CLI side, parseMidTurnDrainResponse caps processing at MAX_MID_TURN_DRAIN_ITEMS = 10 via capMidTurnDrainItems. Entries 11+ are removed from the agent's queue, marked "accepted" in the desktop UI, but never injected into the LLM conversation. Users have no indication their messages were lost.

Suggested fix: Apply the cap in the agent's drain handler before splicing, so unprocessed entries remain queued for the next drain cycle. Alternatively, acknowledge only the entries actually included in the response.

— qwen3.7-max via Qwen Code /review

@@ -2427,16 +2461,128 @@ export const useGeminiStream = (
? []
: (midTurnDrainRef?.current?.() ?? []);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] UI-side drain has no item cap. midTurnDrainRef?.current?.() returns all drained messages and the for loop iterates over all of them. The ACP path enforces MAX_MID_TURN_DRAIN_ITEMS = 10 in Session.ts, but the UI path has no equivalent limit. While human typing naturally limits queue depth, paste operations or programmatic input could produce larger batches.

Suggested change
: (midTurnDrainRef?.current?.() ?? []);
const MAX_UI_MID_TURN_DRAIN_ITEMS = 10;
const drained = (midTurnDrainRef?.current?.() ?? []).slice(0, MAX_UI_MID_TURN_DRAIN_ITEMS);

— qwen3.7-max via Qwen Code /review

value['mimeType'].startsWith('audio/') &&
typeof value['data'] === 'string'
);
case 'resource_link':

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] isContentBlock rejects resource_link blocks (return false), but #resolvePrompt at line 4307 fully handles them (resolving URIs via readManyFiles, falling back to @<uri> text). This validation/resolution inconsistency means resource_link content blocks in mid-turn drain items are silently filtered out by getValidMidTurnContentBlocks before #resolvePrompt ever sees them. The ACP spec states "All agents MUST support ContentBlock::ResourceLink in prompts."

Suggested change
case 'resource_link':
case 'resource_link':
return typeof value['uri'] === 'string' && typeof value['name'] === 'string';

— qwen3.7-max via Qwen Code /review

return typeof value['blob'] === 'string';
}

function hasInlineMediaContentBlock(content: ContentBlock[]): boolean {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] hasInlineMediaContentBlock only checks for image or audio types. When #resolvePrompt fails for a message with only resource blocks (no images/audio), this function returns false and the [Attachment could not be processed] marker is NOT appended. The user and model see only the display text with no indication that resource content was lost. Consider broadening the check to cover any non-text content block type.

— qwen3.7-max via Qwen Code /review

}
}

function isEmbeddedResourceResource(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] isEmbeddedResourceResource caps text at MAX_MID_TURN_RESOURCE_TEXT_LENGTH = 100_000 characters but the blob field has no size limit. Similarly, isContentBlock doesn't cap data for image/audio blocks. A malicious or buggy ACP client could send arbitrarily large base64 blobs that get decoded and processed without bounds. Consider adding a MAX_MID_TURN_RESOURCE_BLOB_LENGTH constant and validating data.length / blob.length.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

✅ Local runtime verification (Linux, real end-to-end)

I ran a real end-to-end verification of this PR on Linux — building the actual dist/cli.js and driving it through a mock OpenAI provider (which records the exact on-the-wire request payload), a real ACP ClientSideConnection, and the real Ink TUI over a tmux pty. For each path I ran an A/B counterfactual (PR build vs. pre-PR baseline) so the result isolates this PR's change.

Result: both user-facing paths preserve the mid-turn image to the model after this PR, and lose it on the pre-PR baseline. Legacy text-only drains stay backward-compatible. The 275 focused unit tests pass.

Environment

OS 🐧 Linux (Node v22.22.2, tmux 3.5a)
PR head 855a2686c · base (merge-base) 3ff7e7f95
Build npm installnode dist/cli.js (verified bundle carries parseMidTurnDrainResponse / prefixMidTurnUserMessageParts)
Provider local mock OpenAI: turn 1 returns a run_shell_command tool-call, the continuation returns final text; model id gpt-4o so image inlineData serializes to an image_url data URL
Image probe 1×1 PNG, base64 begins iVBORw0KGgo… (greppable end-to-end)

1) CLI path — mid-turn @image during tool execution

useGeminiStream.ts · atCommandProcessor.ts (resolveAtCommandQuery) · midTurnUserMessage.ts

Scenario (real TUI in tmux): submit a prompt → model returns a long-running run_shell_commandwhile the shell tool is still executing, type @midturn-image.png inspect this image and submit. The TUI shows ● … / Press ↑ to edit queued messages, confirming it entered the mid-turn queue. When the tool finishes, the queue is drained and the continuation request is sent.

Build mid-turn message image_url reaches model? mid-turn text?
This PR @midturn-image.png inspect this image yes — resolved file → image_url (our PNG) ✅ prefixed
pre-PR (counterfactual¹) same no — sent as literal @… text, image lost ✅ literal text

Continuation request (role: "tool" result is [3]; injected user message is [4]):

This PR — the @ reference is resolved into structured parts and the image is attached:

[4] role=user:
    text("\n[User message received during tool execution]: @midturn-image.png ins…")
    text("\n--- Content from referenced files ---")
    text("\nContent from /…/midturn-image.png:\n")
    IMAGE_URL(data:image/png;base64,iVBORw0KGgoAAA…)   ← image preserved
    text("\n--- End of content ---")

pre-PR — the same message collapses to plain text, no resolution, no image:

[4] role=user:
    text("\n[User message received during tool execution]: @midturn-image.png inspect this image")

¹ counterfactual = a one-line surgical toggle that disables only the new mid-turn @-resolution branch, then rebundle (everything else identical to PR head).


2) ACP path — structured mid-turn drain

Session.ts (#drainMidTurnUserMessages / parseMidTurnDrainResponse / #resolvePrompt)

Scenario (real ACP client over stdio): a ClientSideConnection drives initialize → newSession → prompt; the agent runs a tool, then calls the craft/drainMidTurnQueue ext-method, which my client answers with a configurable shape. I assert what lands in the continuation request to the model.

Build drain response image_url reaches model? mid-turn text?
This PR {items:[{content:[text, image]}]} (new) yesimage_url (our PNG) ✅ prefixed
This PR {messages:["…"]} (legacy) ❌ no (no image sent) ✅ prefixed — backward-compatible
pre-PR {items:[{content:[text, image]}]} (new) ❌ no whole message dropped

Decisive evidence — with the PR, the structured items image is injected as a user message right after the tool result:

[4] { "role": "user", "content": [
  { "type": "text", "text": "\n[User message received during tool execution]: inspect this image" },
  { "type": "image_url", "image_url": { "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA…" } }
]}

On the pre-PR build the same items response is unrecognized (the old code reads only response.messages), so the continuation contains no injected user message at all (messages.length 5 → 4) — the text and the image are silently lost. This is the bug the PR fixes. The legacy {messages} shape still produces the text-only mid-turn message, so existing clients are unaffected.


3) Focused unit tests (vitest, run on PR source)

✓ src/utils/midTurnUserMessage.test.ts        (3 tests)
✓ src/ui/hooks/useGeminiStream.test.tsx        (119 tests)
✓ src/acp-integration/session/Session.test.ts  (153 tests)
  Test Files  3 passed (3)   Tests  275 passed (275)

Scope / not covered here

  • Desktop Qwen backend (SessionManager.ts, qwen-agent.ts, backend/types.ts) — the live-attachment forwarding and the missing-base64 → retain-for-next-turn fallback were not runtime-driven: that path needs the desktop server + a desktop/browser client, which is outside this CLI/ACP harness. Its bun-based unit tests don't resolve standalone in an isolated worktree, so they're left to project CI. Worth a manual desktop pass before merge (the PR marks Windows/Linux untested, too).
  • The [Attachment could not be processed] fallback fires only when clampInlineMediaPart throws; a valid small image never triggers it, so it's a defensive path best left to the unit tests (which cover it).

Conclusion: the CLI and ACP changes do what the PR says — mid-turn image input is preserved through tool execution and reaches the model, while text-only behavior remains backward-compatible. LGTM on those two paths from a runtime standpoint; recommend a manual desktop spot-check for the third.

🇨🇳 中文版(点击展开)

✅ 本地运行时验证(Linux,真实端到端)

我在 Linux 上对本 PR 做了真实的端到端验证:构建了真正的 dist/cli.js,并通过 mock OpenAI provider(记录发往模型的精确 on-the-wire 请求体)、真实的 ACP ClientSideConnection 以及 基于 tmux pty 的真实 Ink TUI 来驱动它。每条路径都做了 A/B 对照(PR 构建 vs. PR 前基线),从而把结果精确归因到本 PR 的改动。

结论:修复后,两条面向用户的路径都能把 mid-turn 图片保留并送达模型;在 PR 前基线上都会丢失。legacy 纯文本 drain 保持向后兼容。275 个聚焦单测全部通过。

环境

系统 🐧 Linux(Node v22.22.2,tmux 3.5a)
PR head 855a2686c · base(merge-base)3ff7e7f95
构建 npm installnode dist/cli.js(已确认 bundle 含 parseMidTurnDrainResponse / prefixMidTurnUserMessageParts
Provider 本地 mock OpenAI:第 1 轮返回 run_shell_command tool-call,续轮返回最终文本;model id 用 gpt-4o,因此图片 inlineData 会被序列化成 image_url data URL
测试图片 1×1 PNG,base64 以 iVBORw0KGgo… 开头(端到端可 grep)

1) CLI 路径 — 工具执行期间的 mid-turn @image

useGeminiStream.ts · atCommandProcessor.tsresolveAtCommandQuery)· midTurnUserMessage.ts

场景(tmux 中的真实 TUI): 提交 prompt → 模型返回一个长时间运行的 run_shell_command在 shell 工具仍在执行时,输入 @midturn-image.png inspect this image 并提交。TUI 显示 ● … / Press ↑ to edit queued messages,说明消息进入了 mid-turn 队列。工具结束后队列被 drain,续轮请求随之发出。

构建 mid-turn 消息 image_url 是否送达模型? mid-turn 文本?
本 PR @midturn-image.png inspect this image @ 解析为文件 → image_url(我们的 PNG) ✅ 带前缀
PR 前(对照¹) 同上 — 作为纯文本 @… 发送,图片丢失 ✅ 纯文本

续轮请求(role:"tool" 结果是 [3],注入的 user 消息是 [4]):

本 PR@ 引用被解析为结构化 parts,图片被附上:

[4] role=user:
    text("\n[User message received during tool execution]: @midturn-image.png ins…")
    text("\n--- Content from referenced files ---")
    text("\nContent from /…/midturn-image.png:\n")
    IMAGE_URL(data:image/png;base64,iVBORw0KGgoAAA…)   ← 图片被保留
    text("\n--- End of content ---")

PR 前 — 同一条消息退化为纯文本,没有解析、没有图片:

[4] role=user:
    text("\n[User message received during tool execution]: @midturn-image.png inspect this image")

¹ 对照 = 仅用一行手术式开关关掉新的 mid-turn @ 解析分支后重新打包(其余与 PR head 完全一致)。

2) ACP 路径 — 结构化 mid-turn drain

Session.ts#drainMidTurnUserMessages / parseMidTurnDrainResponse / #resolvePrompt

场景(stdio 上的真实 ACP 客户端): 一个 ClientSideConnection 驱动 initialize → newSession → prompt;agent 执行工具后调用 craft/drainMidTurnQueue ext-method,我的客户端用可配置的返回结构回应。然后断言续轮请求里发给模型的内容。

构建 drain 返回 image_url 是否送达模型? mid-turn 文本?
本 PR {items:[{content:[text, image]}]}(新) image_url(我们的 PNG) ✅ 带前缀
本 PR {messages:["…"]}(legacy) ❌ 否(不发图片) ✅ 带前缀 — 向后兼容
PR 前 {items:[{content:[text, image]}]}(新) ❌ 否 整条消息被丢弃

关键证据 — 修复后,结构化 items 中的图片会作为 user 消息注入在 tool 结果之后:

[4] { "role": "user", "content": [
  { "type": "text", "text": "\n[User message received during tool execution]: inspect this image" },
  { "type": "image_url", "image_url": { "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA…" } }
]}

PR 前 的构建上,同样的 items 返回无法被识别(旧代码只读 response.messages),续轮里 完全没有注入任何 user 消息messages.length 5 → 4)——文本和图片都被静默丢弃。这正是本 PR 修复的 bug。而 legacy {messages} 形态仍能产出纯文本的 mid-turn 消息,所以现有客户端不受影响。

3) 聚焦单测(vitest,跑在 PR 源码上)

✓ src/utils/midTurnUserMessage.test.ts        (3)
✓ src/ui/hooks/useGeminiStream.test.tsx        (119)
✓ src/acp-integration/session/Session.test.ts  (153)
  Test Files  3 passed (3)   Tests  275 passed (275)

范围 / 本次未覆盖

  • Desktop Qwen backendSessionManager.tsqwen-agent.tsbackend/types.ts)—— live 附件转发与 缺少 base64 → 留到下一轮 的回退路径做运行时驱动:该路径需要 desktop server + desktop/浏览器客户端,超出本 CLI/ACP 测试床范围。其 bun 单测在隔离 worktree 下无法独立解析依赖,故交由项目 CI。建议合并前手动跑一遍 desktop(PR 也标注了 Windows/Linux 未测)。
  • [Attachment could not be processed] 回退仅在 clampInlineMediaPart 抛错时触发;正常小图片不会触发,属于防御性路径,交由单测覆盖(单测已覆盖)。

结论: CLI 与 ACP 两条改动达到了 PR 的目标 —— mid-turn 图片输入能穿过工具执行并送达模型,同时纯文本行为保持向后兼容。从运行时角度这两条路径 LGTM;建议对第三条(desktop)做一次手动抽查。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Stage 2 — Code review + testing

Code review (independent read, then diff)

Before looking at the diff I sketched: the bug is that mid-turn user messages are plain strings at three handoff points (CLI @ resolver → mid-turn drain, ACP drain response, desktop queue). The minimal fix is (a) a helper that can prefix a Part[] instead of a string, (b) an additive protocol bump so the drain can carry structured ContentBlock[] when attachments exist, (c) the desktop queue forwarding image attachments + acking by stable id rather than by text, and (d) a timeout/abort so a stuck resolution can't wedge the tool boundary. That's essentially what the PR does; nothing in my sketch was simpler than what's here.

Reading the diff against that baseline, the things that matter:

  • Security — solid. parseMidTurnDrainResponse in Session.ts has strict isContentBlock validation: rejects resource_link (closes the file:/// mid-turn injection vector), enforces image/* / audio/* mime prefixes (blocks text/html disguised as media), caps embedded-resource text at 100 KB, and caps total drain items at 10. Both the injection-prevention and the resource-spoofing cases are covered by red/green-confirmed tests. This is the single most important thing to get right in a mid-turn code path, and it's right.
  • Protocol extension — backward compatible. New items shape alongside legacy messages; legacy clients keep getting text-only drains; the onMidTurnMessagesDrained callback shifts from string[] to string[] of ids (still named the same, but semantically different — worth a line in the PR notes for downstream ACP consumers, not a blocker).
  • Abort/timeout handling — correct. Per-message 10 s resolution timeout via AbortSignal.any + parent signal; sequential loop with early-return on abort is what makes "stops draining when aborted" actually hold (the open Promise.allSettled suggestion in the existing review thread would break that guarantee — maintaining the sequential path is the right call).
  • atCommandProcessor refactor — clean. Splits resolveAtCommandQuery (pure resolution) from handleAtCommand (resolution + recording + history insertion). The mid-turn path now reuses resolution without triggering the side effects that belong only to the top-level submit. No behavior change on the normal @ path — atCommandProcessor.test.ts 43/43 green confirms it.
  • Desktop fallback queue path — correct. Failed buildPromptBlocks entries are re-queued with a failure counter and dropped as a plain-text + [Attachment could not be processed] fallback after 3 attempts — bounded, no infinite retry, no silent loss.
  • Ack-by-id. enqueueMidTurnMessage now takes metadata: { messageId?, optimisticMessageId? } and the drain returns ids (messageId → optimisticMessageId → raw text). Stable ack keys mean the host won't double-replay after a reconnection, which was the bug the PR explicitly calls out.

No correctness bugs, no AGENTS.md-level violations, no drive-by refactors past the atCommandProcessor split (which is load-bearing for the mid-turn reuse, so it's in-scope).

Testing — independent Linux verification on PR head 855a2686c

I brought up a worktree at the PR head in this Linux runner, ran npm ci, and drove the changed CLI suites in tmux. The desktop suite needs Bun + a seeded CRAFT_CONFIG_DIR; the maintainer's independent Linux run (separate worktree, Bun 1.3.14, seeded config-defaults.json) already covered it below, so I cite that as authoritative and didn't duplicate it here.

tmux capture — CLI suites, PR head, Linux

 RUN  v3.2.4 /home/runner/work/qwen-code/qwen-code/.qwen/worktrees/triage/packages/cli
      Coverage enabled with v8

 ✓ src/ui/hooks/atCommandProcessor.test.ts (43 tests) 174ms
 ✓ src/utils/midTurnUserMessage.test.ts (3 tests) 5ms
 ✓ src/ui/hooks/useGeminiStream.test.tsx (119 tests) 1340ms
 ✓ src/acp-integration/session/Session.test.ts (153 tests) 16782ms
   ✓ Session > prompt > auto-compress > latches mid-turn drain off after repeated timeouts when the client never responds  6020ms
   ✓ Session > prompt > auto-compress > resets the timeout strike count when a drain succeeds  8020ms

 Test Files  4 passed (4)
      Tests  318 passed (318)
   Start at  01:10:13
   Duration  29.03s (transform 6.20s, setup 604ms, collect 21.36s, tests 18.30s, environment 2.33s, prepare 577ms)

tmux capture — typecheck, PR head, Linux

$ npx tsc --noEmit -p packages/cli/tsconfig.json | tail -15
$  # (silent = clean, exit 0)

So on the CLI surface alone: 318 tests passed across the four touched suites (3 new in midTurnUserMessage.test.ts, the rest across useGeminiStream, Session, and the newly split atCommandProcessor), and tsc --noEmit is clean.

CI — all green on the PR

Classify PR ✅ · CodeQL ✅ · Lint ✅ · Test (ubuntu-latest, Node 22.x) ✅ 16 m 43 s · Test (macos-latest, Node 22.x) ✅ 15 m 15 s · Test (windows-latest, Node 22.x) ✅ 26 m 21 s. Only review-pr (a bot) remains pending — not a merge gate.

Cross-referenced maintainer verification (Linux, same head 855a2686c)

The maintainer's earlier comments on this PR already provide two pieces of evidence I couldn't practically reproduce here and that close the remaining gaps:

  1. Red/green. Reverting only the impl files to merge-base 3ff7e7f95 while keeping the PR's test files produces exactly 21 failures — 11 in CLI (6 in useGeminiStream, 5 in Session, plus midTurnUserMessage.test.ts import failure) and 10 in desktop (qwen-native-history + qwen-agent-slash-history) — all 1:1 mapped to the new behavior (mid-turn @ resolution, structured-items drain, content-block validation, ack-by-messageId, live-image offering). The remaining 309 pre-existing tests still pass. So the tests genuinely guard the fix.
  2. Live TUI capture during real tool execution. A scripted mock model returning run_shell_command sleep 30, with @pic.png what is shown in this image queued during the live sleep. Only two requests total were sent, and the continuation request's trailing user message contained image_url: "data:image/png;base64,iVBORw0KGgo…" whose base64 was byte-for-byte identical to the on-disk pic.png. Pre-fix (merge-base code) would have produced the literal text @pic.png what is shown in this image with no image. That's the end-to-end evidence the unit tests model.

One scoping caveat worth a line in the PR (not a bug): the fix's benefit is limited to vision-capable models — a text-only model name replaces the @pic.png with a placeholder upstream of the mid-turn logic at @/read resolution per modalityDefaults.ts. That's correct behavior (you don't want image bytes sent to a text-only model), but a reviewer reproducing it must point the session at a vision-capable model.

Verdict at this stage

Everything I'd want to see before moving to reflection is green: security-sensitive parsing validated by red/green, protocol extension is additive, timeout/abort is bounded, the refactors are in-scope, CI passes on all three platforms, Linux independent run passes, and the live TUI capture closes the "does the image actually reach the model" question with byte-exact evidence. Moving to Stage 3.

中文说明

Stage 2 — 代码审查 + 测试

代码审查(独立阅读,再对照 diff)

看 diff 之前我勾画的方案:bug 的根因是 mid-turn 用户消息在三个交接点都是纯字符串(CLI @ 解析 → mid-turn drain、ACP drain 响应、desktop 队列)。最小修复是:(a) 一个能给 Part[] 加前缀而不是给 string 加前缀的 helper;(b) 加法式协议扩展,让 drain 在有附件时携带结构化 ContentBlock[];(c) desktop 队列转发图片附件并按稳定 id 而非文本 ack;(d) 超时/abort 避免卡死的解析阻塞工具边界。PR 基本就是这么做的,我勾画的也没有比这更简单的路径。

对照 diff 看,重要的几点:

  • 安全——扎实。 Session.tsparseMidTurnDrainResponse 做了严格的 isContentBlock 校验:拒绝 resource_link(堵住 file:/// mid-turn 注入向量);强制 image/* / audio/* mime 前缀(拦截伪装成媒体的 text/html);嵌入式 resource 文本上限 100 KB;drain 总条数上限 10。注入防护和资源伪装两条都有 red/green 确认的测试覆盖。mid-turn 代码路径上这是最关键的点,PR 做得对。
  • 协议扩展——向后兼容。items shape 与 legacy messages 并存;legacy client 继续收到纯文本 drain;onMidTurnMessagesDrainedstring[] 语义改为 id 的 string[](名字没变但语义不同——值得在 PR 说明里给下游 ACP consumer 提一句,非阻塞)。
  • Abort/timeout——正确。 单条消息 10s 解析超时,用 AbortSignal.any 组合父信号;串行循环 + abort 提前返回正是"abort 时停止 drain"这条保证成立的原因(已有 review thread 里那个 Promise.allSettled 建议会破坏这条保证——保持串行是对的)。
  • atCommandProcessor 重构——干净。resolveAtCommandQuery(纯解析)和 handleAtCommand(解析 + 录制 + 历史插入)拆开。mid-turn 路径现在复用解析而不触发仅属于顶层 submit 的副作用。常规 @ 路径行为不变——atCommandProcessor.test.ts 43/43 全绿确认。
  • Desktop 回退队列路径——正确。 buildPromptBlocks 失败的条目带失败计数重新入队;连续 3 次失败后降级为纯文本 + [Attachment could not be processed],有上限,无无限重试,无静默丢失。
  • 按 id ack。 enqueueMidTurnMessage 新增 metadata: { messageId?, optimisticMessageId? };drain 返回 id(messageId → optimisticMessageId → 原始文本)。稳定 ack key 避免重连后重复 replay,正是 PR 自己点出的那个 bug。

没有正确性 bug,没有 AGENTS.md 层面的违规,也没有超出 atCommandProcessor 拆分之外的顺手重构(这个拆分是 mid-turn 复用所必需的,所以属于范围内)。

测试 —— 在 PR head 855a2686c 上独立 Linux 验证

我在这个 Linux runner 上拉了 worktree 到 PR head,跑了 npm ci,在 tmux 下跑改动的 CLI 套件。desktop 套件需要 Bun + 预置的 CRAFT_CONFIG_DIR;维护者此前的独立 Linux 验证(独立 worktree、Bun 1.3.14、预置 config-defaults.json)已经覆盖,下面引用为权威结果,不重复。

tmux 抓取 —— CLI 套件,PR head,Linux

 RUN  v3.2.4 /home/runner/work/qwen-code/qwen-code/.qwen/worktrees/triage/packages/cli
      Coverage enabled with v8

 ✓ src/ui/hooks/atCommandProcessor.test.ts (43 tests) 174ms
 ✓ src/utils/midTurnUserMessage.test.ts (3 tests) 5ms
 ✓ src/ui/hooks/useGeminiStream.test.tsx (119 tests) 1340ms
 ✓ src/acp-integration/session/Session.test.ts (153 tests) 16782ms
   ✓ Session > prompt > auto-compress > latches mid-turn drain off after repeated timeouts when the client never responds  6020ms
   ✓ Session > prompt > auto-compress > resets the timeout strike count when a drain succeeds  8020ms

 Test Files  4 passed (4)
      Tests  318 passed (318)
   Start at  01:10:13
   Duration  29.03s (transform 6.20s, setup 604ms, collect 21.36s, tests 18.30s, environment 2.33s, prepare 577ms)

tmux 抓取 —— 类型检查,PR head,Linux

$ npx tsc --noEmit -p packages/cli/tsconfig.json | tail -15
$  #(静默 = 干净,exit 0)

仅 CLI 面:4 个改动套件共 318 测试通过midTurnUserMessage.test.ts 3 个新增,其余分布在 useGeminiStreamSession、新拆分的 atCommandProcessor),tsc --noEmit 干净。

CI —— 全绿

Classify PR ✅ · CodeQL ✅ · Lint ✅ · Test (ubuntu-latest, Node 22.x) ✅ 16 m 43 s · Test (macos-latest, Node 22.x) ✅ 15 m 15 s · Test (windows-latest, Node 22.x) ✅ 26 m 21 s。仅 review-pr(机器人)pending,不是合并门禁。

交叉引用维护者验证(Linux,同一 head 855a2686c

维护者在此 PR 上早前的两条评论已提供两项我在这里不便重复、且把剩余缺口都补全的证据:

  1. Red/green。 只把 impl 文件回退到 merge-base 3ff7e7f95、保留 PR 测试文件:恰好 21 个失败——CLI 11 个(useGeminiStream 6、Session 5、加 midTurnUserMessage.test.ts 整文件 import 失败)、desktop 10 个(qwen-native-history + qwen-agent-slash-history)——与新增行为一一对应(mid-turn @ 解析、结构化 items drain、content-block 校验、按 messageId ack、live 图片下发)。其余 309 个原有测试仍通过。所以测试确实守护了修复。
  2. 工具真实执行期间的实时 TUI 抓取。 脚本化 mock 模型返回 run_shell_command sleep 30,在 live sleep 期间排入 @pic.png what is shown in this image总共只发出两个请求,continuation 请求的最后一条 user 消息含 image_url: "data:image/png;base64,iVBORw0KGgo…",其 base64 与磁盘上 pic.png 逐字节一致。修复前(merge-base 代码)只会产出字面文本 @pic.png what is shown in this image,没有图片。这就是单元测试所建模的端到端证据。

一个值得在 PR 里写一句的范围说明(非 bug):修复的收益仅限于视觉模型——纯文本模型名会在 mid-turn 逻辑之前的 @/read 解析处(按 modalityDefaults.ts)把 @pic.png 替换成占位符。这是正确行为(不会把图片字节发给纯文本模型),但复现时要把会话指向视觉模型。

本阶段结论

进入 reflection 前该看到的都绿了:安全相关的解析由 red/green 守护;协议扩展是加法式;超时/abort 有上界;重构都在范围内;CI 三平台全过;Linux 独立跑过;live TUI 抓取以逐字节证据回答"图片是否真的到达模型"。进入 Stage 3。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Stage 3 — Reflection + verdict

Stepping back: the PR addresses a real, user-visible loss — images (and other structured attachments) silently dropped from mid-turn user messages at three separate handoff points. It's the kind of bug that erodes trust in the tool exactly when the user is relying on it most (typing while the agent is busy), and the linked issue #5036 shows it was hitting real workflows.

The approach matches the independent sketch I wrote before reading the diff, and I don't see a materially simpler path. The three surfaces (CLI @/drain, ACP protocol, desktop queue) are genuinely coupled — cutting any one of them leaves the bug half-fixed on that surface. The test-heavy ratio (~2500 of +3350 lines are tests) is justified here: the security-sensitive content-block validation, the abort-bounded resolution, and the fallback-requeue path all need tests that guard specific failure modes, not just happy paths, and red/green confirms they do.

On the reflection checklist:

  • Independent proposal vs. PR approach: match. I would have built the same helper, the same additive protocol, the same ack-by-id.
  • Solves a real user problem: yes — mid-turn image loss, reproducible on any vision-model session.
  • Straightforward: yes, given the coupling. The atCommandProcessor split is the only structural move, and it's load-bearing (lets the mid-turn path reuse resolution without the submit-side effects).
  • Every edit necessary: yes. I don't see drive-by refactors or scope creep past the stated goal.
  • Results match the promise: byte-for-byte — the maintainer's live TUI capture on the same head shows the image arriving in the same tool-result continuation as inline base64, while the pre-fix code would have produced the literal @pic.png text. That's the fix, working.
  • Six-months-from-now maintenance: I'd thank the author. The content-block validation is the kind of thing future-me will be grateful for the first time someone tries to sneak a resource_link through mid-turn, and the sequential-with-early-abort drain is easy to reason about.
  • Approving because it's genuinely good, not because I ran out of reasons to say no. The maintainer's verification already closed the two gaps I'd normally insist on (red/green + live reproduction), CI is green on all three platforms, my independent Linux run is green, and the code review turned up no correctness or convention issues.

Two non-blocking notes (mentioned in Stage 1/2, restated for the record)

  1. Commit history. 20 commits with several codex: address PR review feedback (#5183) and mid-turn fixups stacked. Squash at merge time — the PR title and body will survive, the intermediate states don't need to.
  2. The open Session.ts:2102 review thread (Promise.allSettled parallelism suggestion) is a deliberate tradeoff: the sequential + abort-early path is what makes the "stop draining when aborted" guarantee hold. Deferring is right.

Approval

Approving now.

中文说明

Stage 3 — 反思 + 结论

退一步看:PR 修的是真实、用户可感的丢失——图片(及其他结构化附件)在三个独立的 mid-turn 交接点被静默丢掉。这种 bug 恰恰在用户最依赖工具时(agent 正忙时输入)侵蚀信任;关联的 #5036 表明它确实命中了真实工作流。

方案与我看 diff 前写下的独立勾画一致,我也没看到更简单的路径。三个面(CLI @/drain、ACP 协议、desktop 队列)确实彼此耦合——砍掉任意一个都会让该面上的 bug 修了一半。测试占比高(+3350 里约 2500 是测试)在这里是合理的:安全敏感的 content-block 校验、有 abort 上界的解析、回退重新入队的路径,都需要守护特定失败模式的测试,不只是 happy path;red/green 确认这些测试做到了。

反思清单:

  • 独立方案 vs PR 方案: 一致。我也会做同样的 helper、同样的加法式协议、同样的按 id ack。
  • 解决真实用户问题: 是——mid-turn 图片丢失,在任何视觉模型会话上都可复现。
  • 直接: 考虑到耦合性,是的。atCommandProcessor 拆分是唯一的结构性动作,且它是承重墙(让 mid-turn 路径复用解析而不带上 submit 侧的副作用)。
  • 每处改动都必要: 是。没看到超出既定目标的顺手重构或范围蔓延。
  • 结果与承诺一致: 逐字节一致——维护者在同一 head 上的 live TUI 抓取显示图片以 inline base64 搭同一个工具结果 continuation 到达;修复前代码只会产出字面 @pic.png 文本。这就是修复在起作用。
  • 六个月后的维护: 我会感谢作者。content-block 校验是未来有人试图通过 mid-turn 偷塞 resource_link 时,未来的我会感激的东西;串行 + 提前 abort 的 drain 也容易推理。
  • 是因为真的好而 approve,不是因为找不到拒绝的理由。 维护者的验证已经关闭了我通常会坚持的两个缺口(red/green + 实时复现),CI 三平台全绿,我独立的 Linux 运行全绿,代码审查没发现正确性或约定问题。

两条非阻塞备注(Stage 1/2 已提过,记录在此复述)

  1. 提交历史。 20 个 commit,含多个 codex: address PR review feedback (#5183) 和 mid-turn fixup 堆叠。合并时 squash——PR 标题和正文会保留,中间态不需要。
  2. 未解决的 Session.ts:2102 review threadPromise.allSettled 并行建议)是刻意权衡:串行 + abort 提前返回,正是"abort 时停止 drain"保证成立的原因。延后是合理的。

审批

现在 approve。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@wenshao
wenshao merged commit 1e6b9d6 into QwenLM:main Jun 18, 2026
58 checks passed

@doudouOUC doudouOUC left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Post-merge review (qwen3.7-max): no new Critical findings. One Suggestion on mid-turn message loss during cancellation — see inline.

— qwen3.7-max via Qwen Code /review

}

if (
turnCancelledRef.current ||

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Mid-turn messages are permanently lost when the user cancels (Ctrl+C) during async @ command resolution. drainQueue() destructively empties the CLI message queue at line ~2462. The for loop below contains await resolveWithAbort(...) calls, which yield control. If cancelled during this window, this guard discards responsesToSend without calling submitQuery — all drained messages are gone from the queue and never reach the model. No log or notification is emitted.

Consider checking cancellation before consuming the drain (so messages stay queued for the next turn), or re-enqueuing unprocessed messages on cancellation:

Suggested change
turnCancelledRef.current ||
// Check cancellation BEFORE draining, not after
if (turnCancelledRef.current || abortControllerRef.current?.signal.aborted) {
return;
}

— qwen3.7-max via Qwen Code /review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants