Skip to content

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

Open
chiga0 wants to merge 130 commits into
QwenLM:feat/tui-foundation-batchfrom
chiga0:feat/tui-live-session-batch
Open

feat(cli): OpenTUI migration live-session and input batch#10368
chiga0 wants to merge 130 commits into
QwenLM:feat/tui-foundation-batchfrom
chiga0:feat/tui-live-session-batch

Conversation

@chiga0

@chiga0 chiga0 commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Third batch of the OpenTUI migration landing plan (#8662), stacked on the foundation-modules batch: the live-session and input layer of the OpenTUI renderer. It adds the live-session stream fold and its model, message rendering (streaming markdown heal, progressive MCP displays, client tool runs, text batching), the transcript adapter with resume and session-switch support, sticky todos, the composer (view, key handling, input model), mouse rows and scrollbar, unified-diff rendering, and the session-compaction notice. Thirty-six files, about 10.3k lines including tests; 280 new tests, all green.

Everything is additive: no reachable ink code path is touched, nothing imports these modules yet, and the default renderer stays ink. The batch is self-contained on top of the foundation modules (theme, event adapter, input history, slash dispatch, mouse hit-testing) and adds no dependency on the dialog or backend batches that follow.

Two landing notes. This batch carries the first consumer of the markdown-heal dependency that was deferred from the infra batch; it lands in devDependencies per the renderer-deps convention used there. One of the carried modules, the transcript adapter, has no dedicated test file on the implementation branch; its conversion logic is exercised through the resume-mapping tests, which replay saved sessions end to end.

Why it's needed

The migration lands batch by batch so each unit is reviewable on its own merits; this is the batch that brings the live conversation — streaming fold, message rendering, and input — into the OpenTUI tree. With the streaming model and foundation services already landed or under review, this batch connects them to actual session rendering and composer behavior, leaving the dialogs, backend composition, and activation batches to complete the path to a flag-reachable renderer.

Risk & Scope

Main risk is bounded by construction: all modules are unreachable from the running CLI until a later batch wires the renderer dispatch, so the blast radius of a defect here is zero for current users; the default (ink) path is byte-for-byte unchanged. Not validated in this PR: rendering behavior on a real terminal (requires the activation batch to make the renderer reachable), and Windows behavior of the mouse-row/scrollbar geometry (CI covers type-level correctness only). No breaking changes, no migration notes — additive only. The markdown-heal dependency is new to the tree (devDependencies, first consumer lands here per the deferred-dependency plan recorded in the infra batch).

Reviewer Test Plan

All new code is unreachable from the running CLI (no wiring yet), so there is no user-visible behavior change to exercise; verification is build, types, and the test suite.

How to verify

Build all workspaces and run the typecheck — both are clean. Run the dependency-direction gate (npm run check:tui-dep-direction): it passes, confirming the new renderer modules keep the framework-neutral boundary intact. Run the CLI test suite: the full packages/cli suite is green, with the OpenTUI subset at 42 files / 619 tests. To confirm the ink path is untouched, note the diff adds files only under the OpenTUI directory plus the renderer dependency in the CLI package manifest and its lockfile.

Tested on

  • macOS 15 (arm64, local): full workspace build, typecheck, lockfile check, dependency-direction gate, full packages/cli suite, OpenTUI subset 42 files / 619 tests, one mutation spot-check (markdown-heal disabled → 3 tests fail as expected).
  • Linux / Windows: not exercised locally; covered by CI (Test jobs run on ubuntu/windows runners).

Evidence (Before & After)

N/A — no user-visible change; modules are not yet wired into the renderer dispatch.

Linked Issues

Checklist


中文说明:OpenTUI 迁移第三批(Live-session & input)——实时会话折叠与模型、消息渲染(流式 markdown 修复、MCP 渐进显示、工具运行、文本批处理)、转录适配(含 resume/会话切换)、粘性待办、输入框(视图/按键/模型)、鼠标行与滚动条、diff 渲染、会话压缩提示。全部为增量代码,不触碰 ink 可达路径,默认渲染器仍为 ink。stacked 在基础模块批(#10146)之上;本批首次引入基建批延迟的 markdown 修复依赖(置于 devDependencies,沿用渲染器依赖约定)。构建、类型检查、依赖方向门禁全过,全量 CLI 测试绿,OpenTUI 子集 42 文件 / 619 用例。

易良 and others added 6 commits August 28, 2026 03:05
…10124)

* refactor(core,cli): rename Gemini residue in memory/spinner/leaf ids

PR 1 of QwenLM#4063 item 6 (de-Google naming). Renames three independent families plus the leaf LLM types:

- Memory filename: GeminiMd* -> Memory* (project memory file, not an LLM client)

- UI spinners: GeminiRespondingSpinner/GeminiSpinner -> RespondingSpinner/Spinner

- Leaf types: GeminiCodeRequest/GeminiChatSendOptions/GeminiErrorEventValue/GeminiFinishedEventValue -> Llm*

- geminiRequest.ts -> llm-request.ts (and its collocated test)

No behavior change. Renamed symbols typecheck clean in core+cli; eslint clean on renamed files.

Refs QwenLM#4063

* fix(cli): resolve rename build failure

* docs(serve): fix memory filename references

* test(cli): pin primary workspace QWEN.md init fallback

Assert that the primary daemon workspace service receives the
hard-coded 'QWEN.md' context filename when boot settings carry no
context.fileName. Previously only the secondary workspace's explicit
SECONDARY.md resolution was asserted, so swapping the fallback literal
at the createDaemonWorkspaceService call site survived the suite.

* refactor(core,cli): finish Gemini residue rename in memoryDiscovery

Complete the rename flagged in review: GeminiFileContent -> MemoryFileContent (module-local interface), includeDirectoriesToReadGemini -> includeDirectoriesToReadMemory (parameter only; all call sites are positional, zero cross-package impact), plus test-local variable names and the stale ORIGINAL_GEMINI_MD_FILENAME test title.

* docs(design): route loadHierarchicalGeminiMemory to Memory naming

Per exception #1 the Llm prefix is reserved for the generic LLM-client surface; the symbol is a memory-file loader (thin wrapper around core's loadServerHierarchicalMemory), so the PR-2 symbol map targets loadHierarchicalMemory instead of loadHierarchicalLlmMemory. Doc-only: the code symbol is not renamed by this PR.

* docs(cli): narrow extractContextFilename fallback description

The undefined fallback first inherits the primary workspace's configured context.fileName snapshot (contextFilenameForInit) at the secondary startup and dynamically added workspace call sites, before the hard-coded QWEN.md. Describe the actual chain instead of the hard-coded default only. Comment-only: the inheritance behavior predates this PR and is unchanged.

* refactor(cli): rename loadHierarchicalGeminiMemory to loadHierarchicalMemory

The design doc's symbol map routes the memory loader to
loadHierarchicalMemory (memory family, exception #1), but no phasing
bullet performed the rename and a prior round left the mixed signature.
Complete the rename across the definition (config.ts), the AppContainer
call site, and the AppContainer test mocks, and update the design doc's
exception #1, symbol map, and PR-1 phasing bullet so the map row is no
longer orphaned.

* fix(core): preserve Gemini rename compatibility

* docs(core): extend Gemini deprecation window

* refactor(core,cli): rename Gemini LLM identifiers

* fix(core): retain Gemini content generator aliases

* docs(core): document legacy content generator paths

* ci: trigger checks after retargeting to main

* test(cli): update renamed LLM expectations

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Third landing batch of the OpenTUI migration (QwenLM#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.
…wenLM#10337)

* fix(test): isolate integration tests from the host global qwen dir

The integration suites spawn the real CLI without isolating the global
qwen dir, so whatever sits in the host's `~/.qwen` shapes the run. Hosted
runners have an empty one and never noticed; the persistent pool does
not, and there the saved memories left by earlier jobs made managed
auto-memory recall issue its own model request ahead of the agent's first
turn. The SDK suites script their fake model server by request index, so
that extra request shifted every index: each scripted tool call landed on
the recall selector and the turn under test got the trailing text
instead, failing 42 cases across permission control and tool control on
both Linux legs while macOS and the other shards stayed green. The same
reds reproduce on any developer machine that has saved memories.

Give each run its own global qwen dir, carrying the host's configuration
across but none of the files that dir accumulates. Configuration has to
come along whole: the suites that talk to a real model rely on ambient
auth, which can live in the credentials block, in the environment block,
or in the model routing, and the persistent pool's own credential source
is not knowable from here. The accumulated files - saved memories,
tool-usage history, extensions, skills, commands - are what a run has no
business depending on, and dropping them is what makes the suites
deterministic again.

* fix(test): keep scratch-home cleanup from failing an all-green run

Removing the run's scratch qwen dir threw when a CLI child that outlived
its test was still writing under the debug directory: the removal walk
reached a directory that refilled between its listing and its rmdir, and
the ENOTEMPTY escaped teardown. Every test had passed, so the shard
reported no failure and still exited red - the same shape as the
memory-file restore before it was made best-effort.

Retry the removal so an ordinary race resolves itself, and warn instead
of throwing when it still cannot finish, so a host that keeps a directory
busy stays diagnosable without failing the run. Sweep scratch homes an
earlier run left behind, past an age floor that clears any run in flight
on the same host, so the persistent pool does not accumulate what a
best-effort cleanup gives up on.

The new case drives the failure through a real writer holding the
directory busy rather than a permission trick, so it does not depend on
the runner's privileges, and it waits for that writer to be producing
before tearing down - without the wait it passes against the very bug it
pins.
…nLM#10149)

* feat(external-context): add configurable Mem0 extension skeleton

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

* fix(external-context): harden Mem0 extension boundaries

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

* fix(extension): harden Mem0 runtime boundaries

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

* docs: align Mem0 design with PR1 scope

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…10355) (QwenLM#10359)

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
…it on running sessions (QwenLM#10302)

* fix(web-shell): keep archive out of the sidebar hover slot and block it on running sessions

The archive action was one of the two default inline buttons that appear
over a session row's metadata slot on hover, right on top of the row's own
click target, so users opening a session could archive it by accident.
Archive now stays in the row's dropdown by default (inlineItems still
accepts 'archive' for hosts that want it back), and it is disabled on any
session with a running turn because the daemon closes the live session
when it archives, which would end that turn.

* test(web-shell): pin the running-session archive guard and capability gate (QwenLM#10302)

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: Qwen-Coder <qwen-coder@alibabacloud.com>
@qwen-code-ci-bot

ghost commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

* fix(serve): persist skill toggles without catalog validation

* fix(web-shell): reconcile skill toggle status

* fix(skills): reconcile settings-only toggles

* fix(serve): version skill settings capabilities

* fix(serve): clarify skill settings outcomes

* docs(serve): clarify skill settings contracts

* docs(serve): align skill toggle design semantics

* docs(serve): distinguish skill changes from activation

* docs(serve): clarify deferred skill refreshes

* fix(web-shell): allow model-only skill settings writes

ghost 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.

@chiga0 Thanks for the third batch — the write-up of the changes themselves is thorough, but the PR body is missing three required sections of the PR template:

  • ### Tested on — the OS test matrix (CI runs on macOS / Windows / Linux; state which environments you verified locally).
  • ## Risk & Scope — main risk or tradeoff, what is not validated, breaking changes / migration notes. For a ~10k-line batch this is exactly the context reviewers need up front.
  • ## Linked Issues — the migration plan (#8662) and the foundation batch (#10146) are mentioned in the prose; they belong in this section.

The sibling foundation batch #10146 fills in all of these sections. Once the PR body is updated to match, a maintainer or author re-run (@qwen-code /triage) will pick it up and continue. Not reviewing code until then.

中文说明

@chiga0 感谢提交第三批!改动本身的描述很详尽,但 PR 正文缺少 PR 模板 中的三个必填部分:

  • ### Tested on —— 操作系统测试矩阵(CI 在 macOS / Windows / Linux 上运行,请说明你本地验证过哪些环境)。
  • ## Risk & Scope —— 主要风险或权衡、未验证的内容、破坏性变更 / 迁移说明。对于约 1 万行的批量改动,这正是评审者最需要前置看到的信息。
  • ## Linked Issues —— 迁移计划(#8662)和基础模块批(#10146)已在正文中提到,但应放在该小节里。

同系列的基础批 #10146 完整填写了以上所有部分。补齐正文后,维护者或作者重新触发(@qwen-code /triage)即可继续流程。在此之前不会进入代码审查。

Qwen Code · qwen3.8-max

易良 and others added 4 commits August 28, 2026 07:15
* fix(test): stop shared-runner state leaking into SDK E2E fake servers

permission-control and tool-control E2E fail on every main commit since Linux E2E moved to persistent self-hosted runners (QwenLM#10085): the fake server's scripted responses key off request index 0, but host state leaks extra model requests ahead of the turn's main request, which then only receives 'Done.'.

- Isolate the spawned CLI from the runner's HOME / global ~/.qwen state via a per-test scratch HOME (HOME + QWEN_HOME in options.env, which the SDK merges over process.env).
- Default SDK E2E settings to disable managed auto-memory/dream: recall selection and background extraction issue side model requests against the fake server and were observed consuming scripted responses.
- Serve fake-server responses by matching the turn's user prompt instead of the request index, so any remaining non-turn request cannot desync the sequence. Matching reads raw string/text-block content because JSON.stringify escapes quotes in prompts.

Test-only change. All 52 cases in the two suites pass on a host that reproduces the polluted-runner state; the suite-wide SDK E2E run shows no new failures.

* fix(test): narrow SDK E2E isolation

Keep only the targeted tool-control hardening: redirect QWEN_HOME into the suite scratch directory and disable managed auto-memory and auto-dream. This removes the per-test HOME cleanup and widespread fake-server rewrites while preventing host settings and background model requests from desynchronizing request-index handlers.

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
…red (QwenLM#10260)

* fix(goal): stamp the wind-down hand-off only when its turn was delivered

QwenLM#10132 marked the record's `windDownTurnId` whenever the turn holding
the wind-down permit finished -- reading "the permit was used" as "the
user got the hand-off". QwenLM#10013 established why that inference is wrong
for the objective-updated notice: a system message or a direct user
query can claim a queued continuation's permit and send its own text
under it, so the turn finishes with the prompt never reaching the
model. Hosts therefore mark delivery at the real send site, and only a
delivered turn commits what it carried.

The hand-off now follows the same rule. `finishTurn` stamps the marker
only when the wind-down turn was marked delivered; an undelivered one
leaves the record clean, so the next `queueContinuation` grants the
hand-off again instead of settling `usage_limited` on a hand-off the
user never received (which a resume would not have repaired either,
since the marker is cleared only by a re-arm). The in-memory permit
marker is released either way; it belongs to the permit, not the
outcome.

The wind-down tests that finish the hand-off turn now mark it delivered
first, so they keep meaning "the model saw the hand-off". Two new cases
pin the split: finished-but-undelivered leaves no marker and re-mints
the hand-off; finished-and-delivered stamps it and stops.

Mutation probe: making the stamp unconditional again fails exactly the
undelivered case (145 others green).

* docs(goal): align wind-down comments with the delivered-stamp rule (QwenLM#10260)

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(channels): recover DWS direct messages

* fix(channels): harden DWS direct-message recovery per review (QwenLM#10274)

* fix(channels): contain DWS history-dispatch costs and failures per review (QwenLM#10274)

* fix(channels): close the DWS in-flight double-spend per review (QwenLM#10274)

* test(channels): pin the DWS stale non-direct drop mark per review (QwenLM#10274)

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
@chiga0

ghost commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

ghost commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 28 passed · 0 failed · 28 total

Flakiness gate: ⚠️ timeout — only 3 of 5 rounds fit the 15-minute budget; the completed rounds agreed

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:28 通过 · 0 失败 · 28 总计

抖动门:⚠️ timeout — only 3 of 5 rounds fit the 15-minute budget; the completed rounds agreed

Verification report

PR #10368 verification — feat(cli): OpenTUI migration live-session and input batch

Verdict: merge-ready — 28/28 scripted assertions passed, 0 unexpected failures.
Verified head: 81f13f4ae45c8f6db559cec43acca7f34afe408b (merge 5434f50c3a, base 8e9a6c729558b2870b06b37e651be53ebca9a064).
Single PR commit, locally reachable and matching the metadata snapshot.

中文摘要
  • 结论merge-ready。28/28 脚本化断言通过,0 意外失败。
  • 核心结论(A/B):本批为纯增量(35 个新增文件,仅 2 个 manifest 修改,0 删除),全部位于 packages/cli/src/ui/opentui/;打包产物(dist/,1001 个文件)不含任何本批标记(7 个标记 0 命中,grep 以 ink/react/yargs 阳性对照验证有效),即默认 ink 路径与发布闭包均不受影响。新增代码的测试负载性经变异 A/B 证明:4/4 变异按预测被杀(含作者声称的 "禁用 heal → 3 个测试失败"),前后对照全绿。
  • 门禁:typecheck 退出码 0;依赖方向门禁 PASS;OpenTUI 子集 42 文件 / 619 用例全绿(与 PR 声称完全一致);新增测试合计恰为 280(与 PR 声称一致)。remend 线级探针 8/8 断言通过,未闭合标记的相邻形状均稳定愈合,20k 字符尾段 ≤0.55ms(曲线平坦)。
  • Findings:两条低优先级非阻塞观察——markdown-heal 在本批内无生产导入方(激活批需接线,否则 flicker 修复不会生效);remend@opentui/* 同处 devDependencies 而被生产源码导入(激活批需提升为 dependencies 或保证仅打包分发)。
  • 未覆盖:全量 packages/cli 套件未重跑(PR 自身 CI 覆盖;增量 diff + 零依赖叶子包使风险有界);真实终端渲染与激活行为(模块按设计未接线,无法执行);Windows 鼠标几何(仅类型级);base 侧可达路径行为 A/B 未单独执行(可达源码字节级相同,等价性由结构证明 + 打包闭包探针承担)。

Central claim and A/B

Central claim: this batch is additive-only and unreachable — it adds the live-session/input OpenTUI layer without changing any reachable behavior, and its 280 new tests actually pin the new code.

Because the batch is intentionally unreachable, the load-bearing proof is structural (diff = control) plus a mutation A/B (tests must kill mutants of the new code):

cell oracle result
S1 diff structure git diff --diff-filter head vs base modified = exactly {package.json, package-lock.json} (+1/+8 lines, remend devDep); added = 35 files, all under ui/opentui/; deleted = 0
S2 import graph static + dynamic import grep outside ui/opentui/ 0 importers; only doc comments in base ui/model files (untouched by PR)
S3 shipped closure npm run bundle at head, marker grep over dist/ (1001 files) 0/7 batch markers (healStreamingMarkdown, transcribeSession, batchTextEvents, liveSessionModel, @opentui/core, @opentui/react, remend); grep proven live by positive controls (ink 346, react 55, yargs 11 files)
S4 bundle smoke node dist/cli.js --version exit 0, prints 0.22.2
M-A/B mutation matrix 4 interface-preserving mutants, per-file vitest, git-restore 4/4 killed exactly as predicted; C0/C1 unmutated controls 19/19 green both sides

Witnesses: evidence/01-final-gates-head-all-green.png (cells S1–S4 + gates), evidence/02-mutation-matrix-all-killed.png (M-A/B as printed).

mutation file result verdict
m1-heal-disabled (bypass remend) markdown-heal.ts 3 failed / 3 passed MATCH — reproduces the author's spot-check claim ("3 tests fail") exactly
m2a-subtype-skip-removed transcript-adapter.ts 1 failed / 6 passed MATCH — validates the PR's claim that the adapter is exercised through resume-mapping tests
m2b-status-always-ok transcript-adapter.ts 1 failed / 6 passed MATCH — errored tool-result mapping is pinned
m3-flush-before-nontext-removed text-batcher.ts 1 failed / 5 passed MATCH — positive control in a different file

All kills are behavioral expected-vs-received assertions (e.g. m1: AssertionError: expected '1. **聚合' to be '1. **聚合**'), not import/compile breakage. Raw logs: logs/m*.log, mutation-matrix-run2.log.

Targeted gates (final-gates.sh, 15/15)

gate result
typecheck (all workspaces + integration) exit 0
dep-direction gate (check:tui-dep-direction.mjs) PASS (1302 core + 4 ui/model files, 0 violations)
OpenTUI subset at head 42 files / 619 tests green — exact match of the PR's claim
new-test tally across the 17 added test files 280 — exact match of the PR's claim
lockfile npm ci at HEAD validated remend@​1.3.1 integrity; remend has zero runtime/peer deps (leaf), so the manifest change cannot perturb other suites

Reviewer Test Plan walk-through

plan step outcome
Build all workspaces pre-built at HEAD by the CI contract; npm run bundle additionally exit 0
Typecheck clean verified, exit 0
npm run check:tui-dep-direction passes verified, PASS
Full CLI suite green; OpenTUI subset 42 files / 619 tests subset verified exactly; full suite deferred to PR CI (see Not covered)
Ink path untouched: diff adds files only under OpenTUI + renderer dep verified structurally (S1) and behaviorally via closure (S3)

Findings

F1 (low, non-blocking) — markdown-heal has no production importer in this batch. Grep shows only markdown-heal.test.ts imports it; messages.tsx provides meta/sub-renderer helpers and does not render the assistant body. This is consistent with the stacked plan (body rendering lands in the backend-composition batch), but it means the flicker fix is inert until a later batch routes streaming assistant text through assistantMarkdownForRender. Carried as a wiring dependency for the activation batch; not a defect of this additive PR.

F2 (low, non-blocking) — remend sits in devDependencies while imported by production source. Consistent with the established convention (@opentui/core/@opentui/react are also devDependencies, lines 103–104 vs 128 of packages/cli/package.json), and safe today because the shipped artifact is the esbuild bundle (closure probe S3 shows remend is not even in it). If a non-bundled runtime path ever loads the renderer, the dep must be promoted to dependencies at activation.

Not covered

  • Full packages/cli suite not re-run here: the PR's own CI covers it, and the diff is additive-only with a zero-dependency leaf package added, which bounds the blast radius; the affected subset (42/619) was run twice green.
  • Real-terminal rendering / activation behavior: the modules are unwired by design; nothing can exercise them end-to-end until the activation batch. The PR states this explicitly.
  • Windows mouse-row/scrollbar geometry: type-level only (per PR).
  • Base-side behavioral A/B on the reachable path not run as a separate cell: reachable sources are byte-identical by diff (S1), so equivalence is structural; the bundle closure (S3) and smoke (S4) are the behavioral witnesses.
  • remend behavior beyond the wrapper contract (HTML blocks, tables): the oracle probed the wrapper's contract plus sibling unclosed-marker shapes; upstream semantics were not audited. Supply-chain review limited to npm ci integrity validation and the zero-dep footprint.
  • Unclosed fenced code blocks are left as-is by remend (observed, not corrupted) and nested unclosed markers close the innermost span per frame — deterministic per input, so no flicker; recorded as oracle observations, not defects.

Methodology

Environment: CI verify job (merge-ref checkout, depth 2, pre-built at HEAD, no GitHub token). Harnesses drove the real code: vitest against src/ (no mocks), a tsx probe importing the production markdown-heal.ts wrapper with the real remend (ESM-only; the probe needed a "type":"module" context — a harness-side fix, not a PR issue), npm run bundle + marker grep with positive controls, and git diff/rev-list for structure. Mutations were applied with perl, each kill verified as a behavioral assertion, sources restored via git checkout (tree confirmed clean afterward). Raw logs and harness scripts live in this artifact dir (logs/, mutation-matrix.sh, remend-oracle.mjs, final-gates.sh); evidence PNGs in evidence/.

Flakiness gate log

rounds=5 files=17 skipped=0
file packages/cli/src/ui/opentui/client-tool-run.test.ts: (cd packages/cli) npx --no-install vitest run ./src/ui/opentui/client-tool-run.test.ts
file packages/cli/src/ui/opentui/diff-render.test.ts: (cd packages/cli) npx --no-install vitest run ./src/ui/opentui/diff-render.test.ts
file packages/cli/src/ui/opentui/input-prompt-key.test.ts: (cd packages/cli) npx --no-install vitest run ./src/ui/opentui/input-prompt-key.test.ts
file packages/cli/src/ui/opentui/input-prompt-model.test.ts: (cd packages/cli) npx --no-install vitest run ./src/ui/opentui/input-prompt-model.test.ts
file packages/cli/src/ui/opentui/input-prompt.test.tsx: (cd packages/cli) npx --no-install vitest run ./src/ui/opentui/input-prompt.test.tsx
file packages/cli/src/ui/opentui/live-session-model.test.ts: (cd packages/cli) npx --no-install vitest run ./src/ui/opentui/live-session-model.test.ts
file packages/cli/src/ui/opentui/live-session.test.ts: (cd packages/cli) npx --no-install vitest run ./src/ui/opentui/live-session.test.ts
file packages/cli/src/ui/opentui/markdown-heal.test.ts: (cd packages/cli) npx --no-install vitest run ./src/ui/opentui/markdown-heal.test.ts
file packages/cli/src/ui/opentui/mcp-progressive.test.ts: (cd packages/cli) npx --no-install vitest run ./src/ui/opentui/mcp-progressive.test.ts
file packages/cli/src/ui/opentui/messages.test.tsx: (cd packages/cli) npx --no-install vitest run ./src/ui/opentui/messages.test.tsx
file packages/cli/src/ui/opentui/mouse-rows.test.ts: (cd packages/cli) npx --no-install vitest run ./src/ui/opentui/mouse-rows.test.ts
file packages/cli/src/ui/opentui/mouse-scrollbar.test.ts: (cd packages/cli) npx --no-install vitest run ./src/ui/opentui/mouse-scrollbar.test.ts
file packages/cli/src/ui/opentui/resume-session.test.ts: (cd packages/cli) npx --no-install vitest run ./src/ui/opentui/resume-session.test.ts
file packages/cli/src/ui/opentui/session-compaction.test.ts: (cd packages/cli) npx --no-install vitest run ./src/ui/opentui/session-compaction.test.ts
file packages/cli/src/ui/opentui/session-switch.test.ts: (cd packages/cli) npx --no-install vitest run ./src/ui/opentui/session-switch.test.ts
file packages/cli/src/ui/opentui/sticky-todos.test.ts: (cd packages/cli) npx --no-install vitest run ./src/ui/opentui/sticky-todos.test.ts
file packages/cli/src/ui/opentui/text-batcher.test.ts: (cd packages/cli) npx --no-install vitest run ./src/ui/opentui/text-batcher.test.ts


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/cli/src/ui/opentui/client-tool-run.test.ts: PPPP
  packages/cli/src/ui/opentui/diff-render.test.ts: PPPP
  packages/cli/src/ui/opentui/input-prompt-key.test.ts: PPPP
  packages/cli/src/ui/opentui/input-prompt-model.test.ts: PPPP
  packages/cli/src/ui/opentui/input-prompt.test.tsx: PPPP
  packages/cli/src/ui/opentui/live-session-model.test.ts: PPPP
  packages/cli/src/ui/opentui/live-session.test.ts: PPP
  packages/cli/src/ui/opentui/markdown-heal.test.ts: PPP
  packages/cli/src/ui/opentui/mcp-progressive.test.ts: PPP
  packages/cli/src/ui/opentui/messages.test.tsx: PPP
  packages/cli/src/ui/opentui/mouse-rows.test.ts: PPP
  packages/cli/src/ui/opentui/mouse-scrollbar.test.ts: PPP
  packages/cli/src/ui/opentui/resume-session.test.ts: PPP
  packages/cli/src/ui/opentui/session-compaction.test.ts: PPP
  packages/cli/src/ui/opentui/session-switch.test.ts: PPP
  packages/cli/src/ui/opentui/sticky-todos.test.ts: PPP
  packages/cli/src/ui/opentui/text-batcher.test.ts: PPP

verdict: timeout
summary: only 3 of 5 rounds fit the 15-minute budget; the completed rounds agreed

--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/cli/src/ui/opentui/client-tool-run.test.ts: P (exit 0)
round 1 · packages/cli/src/ui/opentui/diff-render.test.ts: P (exit 0)
round 1 · packages/cli/src/ui/opentui/input-prompt-key.test.ts: P (exit 0)
round 1 · packages/cli/src/ui/opentui/input-prompt-model.test.ts: P (exit 0)
round 1 · packages/cli/src/ui/opentui/input-prompt.test.tsx: P (exit 0)
round 1 · packages/cli/src/ui/opentui/live-session-model.test.ts: P (exit 0)
round 1 · packages/cli/src/ui/opentui/live-session.test.ts: P (exit 0)
round 1 · packages/cli/src/ui/opentui/markdown-heal.test.ts: P (exit 0)
round 1 · packages/cli/src/ui/opentui/mcp-progressive.test.ts: P (exit 0)
round 1 · packages/cli/src/ui/opentui/messages.test.tsx: P (exit 0)
round 1 · packages/cli/src/ui/opentui/mouse-rows.test.ts: P (exit 0)
round 1 · packages/cli/src/ui/opentui/mouse-scrollbar.test.ts: P (exit 0)
round 1 · packages/cli/src/ui/opentui/resume-session.test.ts: P (exit 0)
round 1 · packages/cli/src/ui/opentui/session-compaction.test.ts: P (exit 0)
round 1 · packages/cli/src/ui/opentui/session-switch.test.ts: P (exit 0)
round 1 · packages/cli/src/ui/opentui/sticky-todos.test.ts: P (exit 0)
round 1 · packages/cli/src/ui/opentui/text-batcher.test.ts: P (exit 0)
round 2 · packages/cli/src/ui/opentui/client-tool-run.test.ts: P (exit 0)
round 2 · packages/cli/src/ui/opentui/diff-render.test.ts: P (exit 0)
round 2 · packages/cli/src/ui/opentui/input-prompt-key.test.ts: P (exit 0)
round 2 · packages/cli/src/ui/opentui/input-prompt-model.test.ts: P (exit 0)
round 2 · packages/cli/src/ui/opentui/input-prompt.test.tsx: P (exit 0)
round 2 · packages/cli/src/ui/opentui/live-session-model.test.ts: P (exit 0)
round 2 · packages/cli/src/ui/opentui/live-session.test.ts: P (exit 0)
round 2 · packages/cli/src/ui/opentui/markdown-heal.test.ts: P (exit 0)
round 2 · packages/cli/src/ui/opentui/mcp-progressive.test.ts: P (exit 0)
round 2 · packages/cli/src/ui/opentui/messages.test.tsx: P (exit 0)
round 2 · packages/cli/src/ui/opentui/mouse-rows.test.ts: P (exit 0)
round 2 · packages/cli/src/ui/opentui/mouse-scrollbar.test.ts: P (exit 0)
round 2 · packages/cli/src/ui/opentui/resume-session.test.ts: P (exit 0)
round 2 · packages/cli/src/ui/opentui/session-compaction.test.ts: P (exit 0)
round 2 · packages/cli/src/ui/opentui/session-switch.test.ts: P (exit 0)
round 2 · packages/cli/src/ui/opentui/sticky-todos.test.ts: P (exit 0)
round 2 · packages/cli/src/ui/opentui/text-batcher.test.ts: P (exit 0)
round 3 · packages/cli/src/ui/opentui/client-tool-run.test.ts: P (exit 0)
round 3 · packages/cli/src/ui/opentui/diff-render.test.ts: P (exit 0)
round 3 · packages/cli/src/ui/opentui/input-prompt-key.test.ts: P (exit 0)
round 3 · packages/cli/src/ui/opentui/input-prompt-model.test.ts: P (exit 0)
round 3 · packages/cli/src/ui/opentui/input-prompt.test.tsx: P (exit 0)
round 3 · packages/cli/src/ui/opentui/live-session-model.test.ts: P (exit 0)
round 3 · packages/cli/src/ui/opentui/live-session.test.ts: P (exit 0)
round 3 · packages/cli/src/ui/opentui/markdown-heal.test.ts: P (exit 0)
round 3 · packages/cli/src/ui/opentui/mcp-progressive.test.ts: P (exit 0)
round 3 · packages/cli/src/ui/opentui/messages.test.tsx: P (exit 0)
round 3 · packages/cli/src/ui/opentui/mouse-rows.test.ts: P (exit 0)
round 3 · packages/cli/src/ui/opentui/mouse-scrollbar.test.ts: P (exit 0)
round 3 · packages/cli/src/ui/opentui/resume-session.test.ts: P (exit 0)
round 3 · packages/cli/src/ui/opentui/session-compaction.test.ts: P (exit 0)
round 3 · packages/cli/src/ui/opentui/session-switch.test.ts: P (exit 0)
round 3 · packages/cli/src/ui/opentui/sticky-todos.test.ts: P (exit 0)
round 3 · packages/cli/src/ui/opentui/text-batcher.test.ts: P (exit 0)
round 4 · packages/cli/src/ui/opentui/client-tool-run.test.ts: P (exit 0)
round 4 · packages/cli/src/ui/opentui/diff-render.test.ts: P (exit 0)
round 4 · packages/cli/src/ui/opentui/input-prompt-key.test.ts: P (exit 0)
round 4 · packages/cli/src/ui/opentui/input-prompt-model.test.ts: P (exit 0)
round 4 · packages/cli/src/ui/opentui/input-prompt.test.tsx: P (exit 0)
round 4 · packages/cli/src/ui/opentui/live-session-model.test.ts: P (exit 0)

Evidence images

01-final-gates-head-all-green

02-mutation-matrix-all-killed

03-remend-oracle-wire-probe

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@qwen-code-ci-bot

ghost commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the third batch — the template gaps from the earlier gate are fixed, so this clears Stage 1. A few notes before code review:

Problem: not a bug fix — this is the live-session & input batch of the OpenTUI migration tracked in #8662, where the maintainer direction call (2026-08-26) approved Phase 1 batch landings: additive-only PRs, flag-gated, ink default untouched. This batch matches that shape.

Direction: aligned. The batch follows the ground rules set on the tracking issue (each batch lands on its own review merits; acceptance criteria per PR). One flag: the stack base #10146 (foundation modules) is still open with changes requested — this PR can't land ahead of it, and any rework there may ripple into this batch's imports.

Size: 5,361 production-logic lines / 4,891 test lines / 9 lines manifest + lockfile (18 production modules, 17 test files, all under packages/cli/src/ui/opentui/). No core-infrastructure paths are touched, so no escalation on that axis — but this is well past the 1,000-line large-PR mark. It's informational only: the batch split in #8662 already cut this as small as the plan allows, and further splitting would fragment a single reviewable unit. Noting it for the maintainer's awareness regardless.

Approach: the scope reads as one coherent unit — the streaming fold + model, message rendering, transcript adapter (resume/session-switch), composer, mouse/scrollbar, diff rendering, todos, compaction notice. All additive, nothing wired into the reachable renderer yet, which keeps the blast radius at zero until the activation batch. One early question I'll take into code review: the author discloses that the transcript adapter has no dedicated test file (its conversion logic is exercised through the resume-mapping tests) — I'll check whether that coverage is actually load-bearing.

Risk: no elevated risk signals — none of the changed files match the repo's revert-correlated high-risk paths. What this PR does not have: any CI test evidence on the head commit. The only workflow runs on this SHA are bot orchestration jobs (triage/review); the fork PR's build/test CI has not run. The suite results quoted in the PR body are the author's own run (macOS/arm64), not independently reproduced. That matters for the review verdict and I'll say so again in Stage 2.

Moving on to code review. 🔍

中文说明

感谢提交第三批——之前模板门禁缺的小节已补齐,Stage 1 通过。进入代码审查前的几点:

问题:不是 bug 修复,而是 #8662 OpenTUI 迁移的 live-session & input 批次。维护者已于 2026-08-26 在跟踪 issue 中批准 Phase 1 分批落地:仅增量、旗标门控、ink 默认路径不动。本批符合该形态。

方向:对齐。批次遵守跟踪 issue 定的基本规则(每批独立按自身评审价值合入、各 PR 有验收标准)。一个提醒:栈基 #10146(foundation modules)仍未合入且被要求修改——本 PR 不能在它之前合入,那边的返工也可能波及本批的 import。

规模:生产逻辑 5,361 行 / 测试 4,891 行 / 清单与 lockfile 9 行(18 个生产模块、17 个测试文件,全部位于 packages/cli/src/ui/opentui/)。未触及核心基础设施路径,不触发该维度的升级——但已远超 1,000 行大 PR 线。仅为信息性说明:#8662 的批次划分已把这批切到计划允许的最小粒度,再拆会把一个可整体评审的单元打碎。仍提请维护者知悉。

方案:范围读起来是一个内聚单元——流式折叠与模型、消息渲染、转录适配(resume/会话切换)、输入框、鼠标/滚动条、diff 渲染、待办、压缩提示。全部增量,尚未接入可达渲染器,在激活批之前爆炸半径为零。带一个初步问题进入代码审查:作者披露转录适配没有专门测试文件(其转换逻辑通过 resume 映射测试覆盖)——我会核实该覆盖是否真正承重。

风险:无升级风险信号——改动文件均未命中本仓库与 revert 相关的高风险路径。本 PR 缺少的是:头提交上没有任何 CI 测试证据。该 SHA 上仅有机器人编排任务(triage/review),fork PR 的构建/测试 CI 未运行。PR 正文引用的测试结果来自作者本机(macOS/arm64),未独立复现。这会影响最终结论,Stage 2 会再次说明。

进入代码审查 🔍

Qwen Code · qwen3.8-max

Reviewed at 81f13f4ae45c8f6db559cec43acca7f34afe408b · re-run with @qwen-code /triage

@qwen-code-ci-bot

ghost commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Code review

I read all 18 production modules in full (5.4k lines) and mapped the test suite (280 cases across 60 describe blocks). Independent baseline first: for this batch I'd have built exactly this shape — a framework-neutral streaming fold, thin render helpers, a transcript adapter for resume, and a composer split into decision model + view — so the architecture matches what I'd propose. The execution is disciplined: every port cites its ink counterpart, the fold is pure (input never mutated), and the stateful session-switch paths carry explicit core-first swap + rollback. No correctness bugs, security holes, or regressions found. Modules are unreachable from the running CLI, so runtime risk of this batch is zero until the wiring batch lands.

Three things worth fixing or tracking, none blocking this additive batch:

  • live-session.tsvoid scheduler.schedule(pending as never, abort). pending is typed as Array<{ callId, name, args? }>, which drops isClientInitiated / prompt_id at the type level and then silences the mismatch with as never. The runtime values are full ToolCallRequestInfo objects minted by the core turn engine (turn.ts builds them with isClientInitiated: false, prompt_id, response_id), so behavior is correct today — but a bare ToolCallRequestInfo[] type would make that guarantee compile-time instead of trust-me. Worth doing before the wiring batch builds on this seam.
  • Transcript adapter coverage. The author discloses there is no dedicated test file; the conversion is exercised through the resume-mapping tests, and I verified those do replay real session shapes end to end (user/thought/tool-call/tool-result pairing, FileDiff structured payloads, error status, subtyped-record skips). A few edges have no direct pin: malformed-JSONL-line skip, the assistant thought→text interleaving close, and the loadTranscriptEvents file seam. Non-blocking; a small dedicated file would close them cheaply.
  • remend in devDependencies. This follows the renderer-deps convention the merged infra batch established (@opentui/core / @opentui/react sit in devDependencies on main for the same reason). One note for the record: packages/cli ships unbundled tsc output ("files": ["dist"]), so when the activation batch wires this renderer in, the renderer deps — @opentui/* and remend — must move to dependencies or get bundled. Fine for this batch since nothing imports the modules yet.

Minor: the live-session.ts header references "PR #8677" while the migration tracks in #8662, and transcript-adapter.ts carries a JSX pragma with no JSX in the file. Trivia, fold into a later pass if convenient.

The live turn loop this batch introduces (the one piece of genuinely new runtime flow, replicating useGeminiStream's tool cycle):

sequenceDiagram
    participant P1 as Backend submit
    participant P2 as livePromptEvents
    participant P3 as GeminiClient stream
    participant P4 as CoreToolScheduler
    participant P5 as Event queue
    P1->>P2: prompt, abort signal
    P2->>P3: sendMessageStream
    P3-->>P2: stream events, tool_call_request
    P2-->>P1: neutral events, folded into history
    P2->>P4: schedule pending calls
    P4->>P5: output chunks, approval requests
    P5-->>P2: drained live as tool-output events
    P4-->>P2: all calls complete
    P2->>P3: functionResponses, loop again or end
Loading
Files changed (30 of 37 shown)
File What changed
package-lock.json lockfile entry for remend 1.3.1, dev:true
packages/cli/package.json adds remend to devDependencies per the renderer-deps convention
packages/cli/src/ui/opentui/live-session-model.ts pure fold of neutral stream events into render-ready history items
packages/cli/src/ui/opentui/live-session.ts live agent-loop event source: drain, schedule tools, submit results, loop
packages/cli/src/ui/opentui/client-tool-run.ts one-shot client-initiated tool runs through CoreToolScheduler
packages/cli/src/ui/opentui/messages.tsx shared message meta and sub-renderers: tool cards, todo rows, ansi grid
packages/cli/src/ui/opentui/markdown-heal.ts thin streaming-markdown heal wrapper over remend
packages/cli/src/ui/opentui/mcp-progressive.ts coalesced mcp-client-update to setTools batch flush
packages/cli/src/ui/opentui/text-batcher.ts coalesces text deltas into one event per frame window
packages/cli/src/ui/opentui/transcript-adapter.ts parses session JSONL records into neutral replay events
packages/cli/src/ui/opentui/resume-session.ts resume wiring feeding the transcript adapter
packages/cli/src/ui/opentui/session-switch.ts /resume and /branch with core-first swap and rollback
packages/cli/src/ui/opentui/session-compaction.tsx compression notice row, spinner then diamond
packages/cli/src/ui/opentui/sticky-todos.ts sticky-todo snapshot extraction from live items
packages/cli/src/ui/opentui/input-prompt-model.ts composer decision model: completion, ranking, paste, esc, history
packages/cli/src/ui/opentui/input-prompt.tsx OpenTUI composer component: textarea, dropdown, key wiring
packages/cli/src/ui/opentui/input-prompt-key.ts raw backspace and printable-key classification
packages/cli/src/ui/opentui/diff-render.ts unified-diff parsing and coloring, ink DiffRenderer port
packages/cli/src/ui/opentui/mouse-rows.ts list-row hover and click dispatch, RowMouseController port
packages/cli/src/ui/opentui/mouse-scrollbar.ts wheel and scrollbar-drag scroll-intent controller
packages/cli/src/ui/opentui/live-session-model.test.ts fold parity tests incl. retry, goal, task events
packages/cli/src/ui/opentui/input-prompt-model.test.ts completion, ranking, paste, submit decisions
packages/cli/src/ui/opentui/input-prompt.test.tsx composer component behavior tests
packages/cli/src/ui/opentui/session-switch.test.ts resume and branch flows incl. rollback paths
packages/cli/src/ui/opentui/resume-session.test.ts end-to-end replay of persisted sessions through the adapter
packages/cli/src/ui/opentui/live-session.test.ts live loop: tool scheduling, approvals, steering drain
packages/cli/src/ui/opentui/client-tool-run.test.ts client-initiated tool scheduling events
packages/cli/src/ui/opentui/text-batcher.test.ts coalescing window and cross-kind ordering
packages/cli/src/ui/opentui/mcp-progressive.test.ts batch-flush coalescing and disposer
…and 8 more paired test files key handling, diff render, mouse, todos, compaction, markdown heal

Testing evidence

This run carries the PR's own CI signal as fetched from the API — and the headline is that there is none on the reviewed commit. The only workflow runs on 81f13f4 are bot orchestration jobs (pull_request_target triage/review plumbing); no pull_request-event build, typecheck, lint, or test run has executed on this head. The table below is exhaustive for real checks:

Check Conclusion
precheck-pr / precheck ✅ success
label ✅ success
Qwen Triage ✅ success
Qwen Pull Request Review (review-pr) ⏳ in progress
(no build / typecheck / lint / unit-test checks exist on this SHA)

So the suite numbers in the PR body — full workspace build clean, typecheck clean, dependency-direction gate passing, packages/cli suite green with the OpenTUI subset at 42 files / 619 tests, and the mutation spot-check (markdown-heal disabled → 3 tests fail) — are the author's own run on macOS 15 arm64, not independently reproduced, and I did not re-run any of it (review is static; PR code is never executed here). Not verified: every build/test claim above on this exact commit. Linux/Windows behavior the author already defers to CI, which has not run either.

The author has write access, so the sandboxed lanes are directly available: the central unverified claim here is the suite itself, not a behavior change (nothing is wired yet — real-scenario rendering is genuinely N/A until the activation batch). Sandboxed verification would settle it: @qwen-code /verify — independently rebuild this head and re-run the typecheck, dependency-direction gate, and the OpenTUI test subset in a clean, risk-screened environment, replacing the author-only numbers above. /tmux earns its keep later, once a batch makes the renderer reachable.

中文说明

代码审查:完整读了全部 18 个生产模块(5.4k 行),并梳理了测试套件(60 个 describe 块、280 个用例)。独立基线先行:这一批我会做成同样的形状——框架无关的流式折叠、薄渲染辅助、resume 用转录适配、决策模型与视图分离的输入框——架构与我的独立方案一致。执行有纪律:每个移植都注明 ink 对应物,折叠是纯函数(不改输入),有状态的会话切换路径带明确的 core 先行交换 + 回滚。未发现正确性 bug、安全漏洞或回归。模块对运行中的 CLI 不可达,在接线批落地前本批运行时风险为零。

三点建议/跟踪项,均不阻塞本增量批:

  • live-session.tsscheduler.schedule(pending as never, abort)pending 类型丢掉了 isClientInitiated/prompt_id,再用 as never 压掉类型错误。运行时值其实是 core turn 引擎生成的完整 ToolCallRequestInfo,行为目前正确——但改成 ToolCallRequestInfo[] 能把这个保证从"信我"变成编译期。建议在接线批之前修。
  • 转录适配的覆盖:作者披露无专门测试文件;我核实 resume 映射测试确实端到端回放真实会话形状。少数边角未直接钉住(畸形 JSON 行跳过、assistant 思考/正文交错收尾、文件读取接缝)。非阻塞,补一个小文件即可。
  • remend 放 devDependencies:沿用已合入基建批的 renderer-deps 约定。备忘:packages/cli 以未打包的 tsc 产物发布,激活批接线时须把 @opentui/*remend 移入 dependencies 或改为打包。本批无碍,因为尚无任何代码 import 这些模块。

小问题:live-session.ts 头注释引用 "PR #8677"(跟踪 issue 是 #8662);transcript-adapter.ts 带了一个无 JSX 的 JSX pragma。琐事,后续顺手处理即可。

测试证据:本次携带的是通过 API 抓取的 PR 自身 CI 信号——要点是被审提交上没有任何项目 CI81f13f4 上仅有机器人编排任务;没有 pull_request 事件的构建/类型检查/lint/测试运行。因此 PR 正文中的套件数字(构建、类型检查、依赖方向门禁、OpenTUI 子集 42 文件 / 619 用例、mutation 抽查)均为作者在 macOS 15 arm64 本机自跑,未独立复现;本审查为静态审查,未执行任何 PR 代码。未验证:以上所有构建/测试结论在本提交上的成立性。作者有 write 权限,可直接触发沙箱通道:@qwen-code /verify 可在干净、风险筛查过的环境里独立重建此头并重跑类型检查、依赖方向门禁与 OpenTUI 测试子集。/tmux 等有批次让渲染器可达后再用。

Qwen Code · qwen3.8-max

Reviewed at 81f13f4ae45c8f6db559cec43acca7f34afe408b · re-run with @qwen-code /triage

@qwen-code-ci-bot

ghost commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — clean, well-executed review of the code itself, but I cannot attest to the test claims and the PR physically cannot merge yet, so this defers rather than approves.

Honest read: this is the best kind of migration batch. The architecture is what I'd have proposed independently — pure event fold, thin render helpers, decision model separated from the composer view, transcript replay as an adapter — and the execution has real discipline behind it: every module cites the ink behavior it reproduces, the fold never mutates its input, the stateful /resume and /branch paths carry explicit core-first swap + rollback, and the 280 tests are aimed at parity rather than line coverage. If I had to maintain this in six months I'd thank the author, not curse them. Nothing in the diff is drive-by; every file belongs to the batch.

So why not approve? Three things I can't talk myself out of:

  1. The central claim is unverified. "Build clean, typecheck clean, dependency-direction gate passing, OpenTUI subset 42 files / 619 tests green" is the author's own macOS run. No project CI has executed on this commit — the fork's pull_request workflows never ran — and this review is static by rule, so I executed nothing. Approving a 10.3k-line fork PR on the strength of self-reported numbers is exactly the attestation the gate exists not to make. The remedy is cheap: fork CI approval on this head, or an author-triggered @qwen-code /verify (write access is present).
  2. The stack base isn't down. feat(cli): OpenTUI migration foundation batch — theme, a11y, clipboard, keys, dialogs scaffolding #10146 (foundation modules) is still open with changes requested. This PR imports its event-adapter / theme / mouse-hit / input-history / slash-dispatch modules, so it cannot land first, and rework there may ripple into this batch.
  3. One seam I'd want fixed before wiring: the as never scheduler cast in live-session.ts (Stage 2). Correct at runtime today, but the wiring batch will build on exactly that seam.

Housekeeping note: the standing CHANGES_REQUESTED review on this PR is my own earlier template gate, which the author has since satisfied — the body now carries every required section. GitHub reviews can't be edited, so it stays on record superseded by this pass; I have not re-submitted it.

Deferred to the maintainer — see the follow-up comment for the named asks.

中文说明

置信度:3/5 —— 代码本身审查干净、执行到位,但我无法为测试结论作证,且 PR 目前物理上无法合入,因此转交维护者而非批准。

坦率地说:这是一个高质量迁移批次的范本。架构与我的独立方案一致——纯事件折叠、薄渲染辅助、决策模型与输入框视图分离、转录回放做成适配器——执行也有真纪律:每个模块都注明复现的 ink 行为,折叠不改输入,有状态的 /resume 与 /branch 路径带明确的 core 先行交换 + 回滚,280 个测试瞄准行为对齐而非行覆盖。六个月后接手我会感谢作者。diff 里没有顺手改动,每个文件都属于本批。

那为什么不批?三点我说服不了自己绕过的:

  1. 核心结论未经验证。"构建干净、类型检查干净、依赖方向门禁通过、OpenTUI 子集 42 文件 / 619 用例全绿"是作者 macOS 本机自跑。该提交上没有任何项目 CI——fork 的 pull_request 工作流从未运行——本审查按规则为纯静态,未执行任何代码。以自报数字批准 1.03 万行的 fork PR,正是门禁不该做的背书。补救很便宜:批准该头的 fork CI,或作者自行触发 @qwen-code /verify(有 write 权限)。
  2. 栈基未落地。 feat(cli): OpenTUI migration foundation batch — theme, a11y, clipboard, keys, dialogs scaffolding #10146(foundation modules)仍 open 且被要求修改。本批 import 其 event-adapter / theme / mouse-hit / input-history / slash-dispatch 模块,不能先合,且那边的返工可能波及本批。
  3. 接线前希望修掉的一个接缝: live-session.ts 里的 as never 调度器强转(见 Stage 2)。目前运行时正确,但接线批恰好会建在这个接缝上。

杂务说明:本 PR 上挂着的 CHANGES_REQUESTED 评审是我早前模板门禁留下的,作者已补齐模板。GitHub 评审不可编辑,故原样留存、以本次审查为准,未重复提交。

已转交维护者——具体请求见后续评论。

Qwen Code · qwen3.8-max

Reviewed at 81f13f4ae45c8f6db559cec43acca7f34afe408b · re-run with @qwen-code /triage

@qwen-code-ci-bot

ghost commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

⏸️ Deferring to @yiliang114 — batch 3 of the OpenTUI migration reviews clean as code, but needs a human call on evidence and sequencing:

  1. No CI evidence on the head. The fork's pull_request workflows haven't run on 81f13f4; the build/typecheck/619-test claims are author-local. Either approving the fork CI runs for this head or an author-triggered @qwen-code /verify would close this.
  2. Stack ordering. The base batch feat(cli): OpenTUI migration foundation batch — theme, a11y, clipboard, keys, dialogs scaffolding #10146 still has changes requested; this PR lands after it, and rework there may ripple into these imports.
  3. One suggested fix before the wiring batch: replace the as never scheduler cast in live-session.ts with a proper ToolCallRequestInfo[] type (details in the Stage 2 comment).

Nothing here is a request-changes on the author — the template gate is satisfied and the code findings are suggestions; this is an evidence + sequencing defer.

Qwen Code · qwen3.8-max

Reviewed at 81f13f4ae45c8f6db559cec43acca7f34afe408b · re-run with @qwen-code /triage

@qwen-code-ci-bot

ghost commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

The bot already has a review of its own on 81f13f4ae45c8f6db559cec43acca7f34afe408b, which still stands.

机器人在 81f13f4ae45c8f6db559cec43acca7f34afe408b 上已有自己的评审,且仍然有效。

The stage comments above were updated with the latest result. View workflow run.

上方各阶段评论已更新为最新结果。查看工作流运行

易良 and others added 5 commits August 28, 2026 08:15
* fix(test): dispatch ACP cron responses by prompt

* test: limit cron dispatch to user prompts
QwenLM#10334)

The `Notify silent triage re-run` step in qwen-triage.yml decides solely
from the review list (HAS_REVIEW / STANDING). A Stage 1-pre duplicate-close
exit terminates a run without leaving any review, so the step cannot tell it
apart from a re-run that did nothing: it POSTs the stage=rerun-summary
comment ("Triage re-run completed without a new review ... it did not") on
the PR the same run just closed, and emits ::warning title=Triage re-run
left no bot review, dispatching a human to a correctly-handled run.

Read the PR's own state before warning: if closedAt is at or after the
trigger comment, the close is this run's terminal action — exit quietly with
no summary comment and no warning. A close BEFORE the trigger keeps the
existing notify behaviour, and a failed `gh pr view` falls through to the
review-list check as before. Only the notify step changes; no other step is
touched.

Tests: scripts/tests/qwen-triage-workflow.test.js gains an executed test
driving the real step body with a stubbed gh in the close-exit shape (no
comment, no warning) and the closed-before-trigger boundary (old behaviour
preserved); the existing executed pin for open PRs is extended to serve the
new pr view call so the pinned no-review notification behaviour is guarded.

Fixes QwenLM#10324

Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com>
The `Check triage response` step of qwen-triage.yml classified any
non-empty response as a successful triage. A model-layer API error
response ("[API Error: Connection error. ...]", 268 chars) is a
non-empty string, so run 33070765162 (triage for QwenLM#10285) reported
success, posted nothing, and no retry or alert fired.

Strip the known rate-limit guidance suffixes, right-trim, and fail the
step when the response ends with the "[API Error: ...]" shape -- the
pattern qwen-code-pr-review.yml already uses for the same CLI output
behavior (the stream-json adapter appends the formatted error last).
This covers both bare error responses and errors appended after partial
output, while a legitimate summary that merely quotes an API error
mid-prose stays green. The existing failure surface then engages: red
run, "ended early" lifecycle comment, re-run path.

Adds executed-step tests in scripts/tests/qwen-triage-workflow.test.js
following the existing check-step harness: the verbatim 268-char
response, an appended-error response, and a quota error with its
guidance suffix must fail; a quoted-error summary, a normal response,
empty, and 'null' keep their existing behavior.

Fixes QwenLM#10314

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…M#10292)

* feat(triage): add duplicate / already-fixed gate (Stage 1-pre)

A PR opened after its linked issue was already fixed by a merged PR
stays open forever: no existing gate looks at the linked issue's
state, and triage has no close action at all. Add a deterministic
Stage 1-pre check that resolves the linked issue's closer via GraphQL
and, only when the PR's production diff is fully subsumed by the
merged fix, posts a bilingual terminal comment and closes the PR.
Any remaining delta requests changes instead; ambiguity escalates to
the maintainer. Register the gate in SKILL.md so it is not treated as
a fabricated policy.

* fix(triage): correct Stage 1-pre subsumption, base scope, and linkage

- Define subsumption over the entire diff (added lines present AND
  deleted lines absent in the default branch) so deletions-only and
  tests-only diffs can never reach the close branch.
- Run Stage 1-pre only for PRs targeting the default branch; backports
  to release/* branches legitimately carry changes already on the
  default branch.
- Extract linked issues via GitHub's closingIssuesReferences instead of
  a keyword grep that missed 6 of 9 closing-keyword forms and matched
  substrings like "prefixes".
- Pin all three invariants in qwen-triage-workflow.test.js.

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

* fix(triage): align Stage 1-pre exits, closer query, and state mechanics

- Name both Stage 1-pre request-changes exits in the terminal gate
  exception and the footer rule, give each an explicit
  `gh pr review --request-changes` command like 1a/1b, and note the
  duplicate-close exit posts its terminal comment and closes instead of
  submitting a review; extend SKILL.md's rejection exception clause to
  cover them.
- Closer query: window the LAST 20 CLOSED_EVENTs and take only the most
  recent close's closer (older closes belong to reopen cycles), null-guard
  the jq filter, and spell out that a failed or empty query means the
  closer is unresolved (a PR number or missing issue must not hard-error).
- Add the per-issue state loop that assigns $N and produces the
  state/stateReason the branch bullets consume.

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

* fix(triage): scope Stage 1-pre linkage to same-repo closing references

The closingIssuesReferences extraction kept only the bare issue number, so a
cross-repo closing reference resolved against this repo's same-numbered
unrelated issue and could drive the wrong-issue gate branches. Filter the
extraction to references whose repository matches the triaged repo and state
that cross-repo closing references are skipped.

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

* fix(triage): guard the Stage 1-pre duplicate close against human reopens

An explicit /triage re-run on a PR a maintainer reopened after a 1-pre
duplicate-close re-derives identical inputs and closes again, indefinitely
overriding the deliberate reopen. Before the close exit posts and closes,
require that no stage=1-pre comment exists yet on the open PR; if one does,
escalate to the maintainer instead of re-closing.

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

* test(triage): pin Stage 1-pre state dispatch and ambiguity prohibition

The per-issue state dispatch (OPEN / CLOSED NOT_PLANNED / CLOSED COMPLETED)
and the "never close on ambiguity" bullet had no test witness: deleting or
inverting either kept the suite green. Add toContain pins binding each state
to its action and one pinning the ambiguity escalation rule.

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

* fix(triage): drop production qualifier from Stage 1-pre exits in SKILL.md

The exit summary qualified both sides with 'production' (remaining production delta -> request-changes; entire production diff fully subsumed -> close), contradicting pr-workflow.md's operational definition: request-changes fires on ANY remaining delta including non-production additions, and a diff with NO production changes is never fully subsumed. A tests-only PR got opposite instructions from the two files. Align SKILL.md with pr-workflow.md and pin the boundary in the suite.

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

* fix(triage): define fixed precedence for mixed linked-issue states in Stage 1-pre

The per-issue loop legend said OPEN -> proceed to 1a while the aggregate bullets said any closed-as-completed issue enters the closer flow, so 'fixes QwenLM#101 and fixes QwenLM#102' with QwenLM#101 OPEN, QwenLM#102 CLOSED-COMPLETED had two contradictory outcomes. Make the loop collect-only and state one fixed precedence (not-planned > completed > all-open), so an OPEN issue never short-circuits a CLOSED one. Pin the precedence.

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

* fix(triage): make Stage 1-pre linkage prose honest about its verification

The doc promised linkage is 'input to verify against the issue's actual state, never as proof by itself', but the issue state check is the only verification, so an accidental prose linkage (e.g. 'resolves QwenLM#123's closer') still drove the branches off an unrelated issue. Reword to state the truth: there is no deterministic intent check, the linkage decides which issues are read, and the blast radius is bounded because close additionally requires the diff to be fully subsumed by the default branch (true only when the change already landed), so an accidental linkage reaches at worst a visible, reversible request-changes or escalation.

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com>
chiga0 and others added 3 commits August 28, 2026 16:45
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
* ci: serialize helper test files

* test(ci): pin helper test serialization
…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
…nLM#9811)

* feat(vscode-ide-companion): reuse WebShell transcript UI behind experimental flag

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* docs(vscode): plan complete Web Shell cutover

* refactor(web-shell): own daemon React bindings

* fix(webui): preserve package entry filenames

* refactor(vscode): complete WebShell UI cutover

* chore(vscode): refresh third-party notices

* fix(vscode): fill embedded chat viewport

* test(web-shell): disambiguate workspace visual locator

* fix(vscode): match embedded chat layout to host

* fix(vscode): compact embedded chat styling

* fix(vscode): align embedded chat density with VS Code

* fix(vscode): complete embedded composer integration

* fix(vscode): restore user message editing after cutover

* fix(vscode): complete WebShell feature parity

* test(vscode-ide-companion): repair host-wiring tests for the WebShell cutover

* refactor(vscode-ide-companion): replace webui build scanner with an ESLint boundary rule

The bespoke recursive source scanner reimplemented a dependency-boundary
check on every extension build. A scoped no-restricted-imports rule
enforces the same boundary on every lint run with less custom code; the
manifest dependency entry was already removed by the cutover.

* fix(web-shell): keep ChatEditor commands prop referentially stable (QwenLM#9811)

The `additionalSlashCommands = []` destructure default allocated a fresh
array on every App render, invalidating the `commands` useMemo and breaking
ChatEditor memoization on every transcript-only re-render. Default to a
module-level constant instead, matching the existing EMPTY_* convention.

Also align the /skills completion expectation with the autoSubmit field the
completion source intentionally emits for leaf skill items.

* fix(vscode): distinguish the VS Code channel and localize its chrome

The companion now drives Web Shell against a shared `qwen serve` daemon,
so the CLI, the browser Web Shell, and this extension all create sessions
in the same workspace catalog. Web Shell recorded `'default'` for every
surface, leaving VS Code conversations indistinguishable from terminal and
browser ones — the panel's history listed sessions the user never opened
here, and nothing attributed a session back to the editor.

Give Web Shell a `sessionSourceType` prop (defaulting to today's
`'default'`) and have the companion stamp `'vscode'` on the sessions it
creates, then scope the history dropdown to that source. The host also
supplies a stable daemon `clientId`, which the bootstrap previously
declared but never sent.

Web Shell localizes its own surface from the `language` signal while the
companion's chrome was hardcoded English, so a zh-CN panel rendered a
Chinese transcript under an English header, history dropdown, onboarding
screen, and account dialog. Route that chrome through a small string table
driven by the same signal, including the host-only slash entries.

Also fix accessibility defects in the history dropdown: rename and delete
were revealed on hover alone and unreachable by keyboard, date headers sat
inside `role="listbox"` as invalid non-option children, arrow-key roving
stopped at group boundaries, `aria-modal` had no focus trap, and a primed
"Delete?" survived both search changes and the pointer leaving the row.

Formatting: `FileMessageHandler` and `SessionMessageHandler` were left
unformatted earlier in this branch and failed the Prettier gate.

* refactor(vscode): drop code orphaned by the WebShell cutover

The webview entry now renders EmbeddedApp against the daemon, which left
the ACP-era hook layer unreachable: nothing imports acpTranscriptAdapter,
useWebViewMessages, useAcpTranscript, useToolCalls, useSessionManagement,
useMessageHandling, useFileContext, useImage, or the permissionTypes added
by this branch. A reachability walk from webview/index.tsx reaches eight
modules; every reference to the rest comes from inside the orphaned set
itself, so it deletes as a closed unit.

EmbeddedWebShell goes with them. It was the host-driven entry point from
the earlier stage of this branch, superseded when EmbeddedApp moved to
WebShellWithProviders, and has had no consumer since — only its own DOM
test and a barrel export.

Also harden the daemon process lifecycle. `start()` returned the cached
runtime without comparing the workspace, so in a multi-root window the
second folder's chat silently reused a daemon bound to the first and
scoped every session, history page, and prompt to the wrong root. Bind the
daemon to its workspace and respawn on a change, keep a superseded child's
late exit from tearing down its successor, and report a post-startup exit
to the webview instead of leaving it fetching against a dead port.

* docs(vscode): describe the daemon architecture the cutover actually ships

The design doc still recorded the plan this branch started from: keep ACP
as the runtime boundary, add no daemon server or loopback port, and treat
"replacing ACP with daemon HTTP/SSE" as a non-goal. The final stage did
exactly that, so the document argued against the code beneath it.

Record the decision and its consequences instead — two processes per
workspace, a daemon shared with the CLI and browser Web Shell, the vscode
source type that keeps the panel's history its own, workspace rebinding in
multi-root windows, and the turn-driven host features that stopped firing.

* fix(vscode): repair round-2 review findings on the web-shell cutover (QwenLM#9811)

- closeDiff now resolves workspace-relative paths the same way showDiff
  does, so permission-cycle diffs opened from daemon-relative paths can
  actually be matched and closed
- a superseded or disposed daemon child no longer reports its exit as a
  crash of the live daemon
- authCancelled no longer hides an already-authenticated session behind
  onboarding; only an unknown auth state settles to unauthenticated
- selection-only activeEditorChanged events no longer undo an explicit
  active-file exclusion
- prepareSubmit dedupes mentions in both path spaces and matches typed
  references on a whole-reference boundary
- permission diffs open only from the SDK's authoritative file_diff
  preview (writes included, model-controlled toolCall mining removed)
- the webview HTML carries VS Code's locale so chrome strings localize
- discontinued qwen-oauth models are no longer re-applied through the
  new-session initial-model route

* fix(vscode): repair round-3 critical findings on the web-shell cutover (QwenLM#9811)

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

* fix(web-shell): import daemon-react-sdk from web-shell instead of webui

The cutover branch dropped the ./daemon-react-sdk export from @qwen-code/webui,
but the TerminalPanel merged in from main still imports it, breaking the
web-shell vite build (Missing "./daemon-react-sdk" specifier). Point the import
and its test mock at @qwen-code/web-shell/daemon-react-sdk, which re-exports the
same useWorkspace hook and matches every other web-shell call site.

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

* fix(vscode): close WebShell UI regression gaps

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

* fix(vscode): initialize WebShell refs explicitly

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

* fix(vscode): narrow queued prompt edits

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

* fix(release): enumerate actual npm workspaces

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

* fix(web-shell): include hasOlderHistory in the render-item callback deps

The renderItem useCallback reads hasOlderHistory to gate the edit action
but omitted it from its dependency array, failing CI's
react-hooks/exhaustive-deps gate.

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

* fix(web-shell): report each connection error once to stop the inline onError re-render loop (QwenLM#10454)

* fix(web-shell): report each connection error once to stop the onError re-render loop

While a connection error persists (e.g. the daemon is unreachable), the
error-notification effect re-fires whenever the onError callback identity
changes. Hosts such as the VS Code embedded app pass an inline onError and
update their own state when it fires, so every notification triggers a host
re-render that hands the effect a fresh callback identity — re-notifying the
same persistent error forever (QwenLM#10406).

Track the last reported connection.error value in a ref and notify only when
the value changes, resetting the tracker once the connection recovers. This
guards every inline-callback consumer, not just memoized hosts.

Fixes QwenLM#10406

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

* fix(web-shell): only stamp the dedup ref once an onError handler exists

Stamping lastReportedConnectionErrorRef before delivery meant a host
that attaches onError after a persistent connection error appeared never
received it: the no-op delivery already marked the error as reported.
Guard on the handler first and add a regression test covering the
late-attach case (red when the guard is removed).

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

* docs(web-shell): document the onError dedup contract and fix comment wording

Describe the reported-once-per-distinct-error semantics, the reset on
recovery, and that replacing the handler mid-error does not re-deliver.
Reword the effect and test comments to describe the host class instead
of naming the VS Code embedded app, which passes a useCallback handler.

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

* test(vscode): mirror the web-shell value-dedup in the EmbeddedApp mock

The WebShellWithProviders mock re-notified on every onError identity
change, mirroring the loop App.tsx can no longer produce. Rewrite it to
report each distinct error value once (resetting on recovery), keep the
loop guard as a regression tripwire, exercise it with a changing
callback identity plus a post-delivery effect re-run, and refresh the
handleShellError comment that still cited the old loop as the
memoization reason.

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

* test(vscode): cast the captured onError prop for the mock wrapper

CapturedProps is an unknown index signature, so the destructured
onError needs the same cast the previous mock applied inline to stay
callable under tsc.

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

* test(vscode): bail the EmbeddedApp mock before stamping when no onError exists

The mirrored dedup effect stamped lastReportedError and counted a
notification even when no handler was attached, while App.tsx returns
before stamping on that path. Add the same early return so a handler
attached mid-error still receives the persistent error, and pin the
no-handler no-stamp behavior with a test that fails if the guard is
removed.

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

---------

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

* fix(web-shell): remove duplicate history dependency

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

* fix(vscode): close remaining WebShell cutover regressions

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

* fix(vscode): keep permission diff handling host-scoped

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

* test(vscode): remove orphaned completion trigger test

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

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: yiliang114 <jinjing.zzj@gmail.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

ghost 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.

⚠️ This run could not certify that any of this diff was reviewed. Suggestions are inline.

Not reviewed: territory review of all 168 diff chunks — chunk agents were never launched; the review time budget was exhausted before the territory fan-out.

Not reviewed: test coverage matrix — prompt was built but the agent was never launched (review time budget).

Not reviewed: cross-file tracer (1c) — prompt was built but the agent was never launched (review time budget).

Not reviewed: whole-file invariant review of the 4 heavily rewritten files — agents were never launched (review time budget).

Not reviewed: the entire diff, the whole-diff test-coverage check, the cross-file consistency pass, the invariant check (state, timers, collections) on packages/cli/src/ui/opentui/slash-dispatch.ts, the invariant check (counters, return values, error taxonomies) on packages/cli/src/ui/opentui/slash-dispatch.ts, the invariant check (config fields, early returns) on packages/cli/src/ui/opentui/slash-dispatch.ts, the invariant check (state, timers, collections) on packages/core/src/core/geminiChat.ts, the invariant check (counters, return values, error taxonomies) on packages/core/src/core/geminiChat.ts, the invariant check (config fields, early returns) on packages/core/src/core/geminiChat.ts, the invariant check (state, timers, collections) on packages/core/src/core/geminiContentGenerator/geminiContentGenerator.ts, the invariant check (counters, return values, error taxonomies) on packages/core/src/core/geminiContentGenerator/geminiContentGenerator.ts, the invariant check (config fields, early returns) on packages/core/src/core/geminiContentGenerator/geminiContentGenerator.ts, the invariant check (state, timers, collections) on packages/web-shell/client/hooks/useAtMentionMenu.ts, the invariant check (counters, return values, error taxonomies) on packages/web-shell/client/hooks/useAtMentionMenu.ts, the invariant check (config fields, early returns) on packages/web-shell/client/hooks/useAtMentionMenu.ts — its prompt was built, but no agent on record was launched with it.

Not reviewed: reverse audit — no auditor was launched with a prompt this skill builds — the pass that hunts what the rest of the review missed ran, if at all, without the method its brief carries.

Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/ci.yml: actionlint embedded-shell source mapping is not yet supported — not linted; the executable-script lint — .github/workflows/e2e.yml: actionlint embedded-shell source mapping is not yet supported — not linted; the executable-script lint — .github/workflows/qwen-triage.yml: actionlint embedded-shell source mapping is not yet supported — not linted; the executable-script lint — .github/workflows/release.yml: actionlint embedded-shell source mapping is not yet supported — not linted.

中文说明

⚠️ 本次运行无法证明这个 diff 的任何部分经过了审查。 建议见行内评论。

未审查:territory review of all 168 diff chunks — chunk agents were never launched; the review time budget was exhausted before the territory fan-out。

未审查:test coverage matrix — prompt was built but the agent was never launched (review time budget)。

未审查:cross-file tracer (1c) — prompt was built but the agent was never launched (review time budget)。

未审查:whole-file invariant review of the 4 heavily rewritten files — agents were never launched (review time budget)。

未审查:整个 diff、全 diff 测试覆盖检查、跨文件一致性检查、不变量检查(状态、定时器、集合)(packages/cli/src/ui/opentui/slash-dispatch.ts)、不变量检查(计数器、返回值、错误分类)(packages/cli/src/ui/opentui/slash-dispatch.ts)、不变量检查(配置字段、提前返回)(packages/cli/src/ui/opentui/slash-dispatch.ts)、不变量检查(状态、定时器、集合)(packages/core/src/core/geminiChat.ts)、不变量检查(计数器、返回值、错误分类)(packages/core/src/core/geminiChat.ts)、不变量检查(配置字段、提前返回)(packages/core/src/core/geminiChat.ts)、不变量检查(状态、定时器、集合)(packages/core/src/core/geminiContentGenerator/geminiContentGenerator.ts)、不变量检查(计数器、返回值、错误分类)(packages/core/src/core/geminiContentGenerator/geminiContentGenerator.ts)、不变量检查(配置字段、提前返回)(packages/core/src/core/geminiContentGenerator/geminiContentGenerator.ts)、不变量检查(状态、定时器、集合)(packages/web-shell/client/hooks/useAtMentionMenu.ts)、不变量检查(计数器、返回值、错误分类)(packages/web-shell/client/hooks/useAtMentionMenu.ts)、不变量检查(配置字段、提前返回)(packages/web-shell/client/hooks/useAtMentionMenu.ts)——它的 prompt 已构建,但没有任何 agent 有记录用它启动过。

未审查:反向审计——没有审计 agent 是用本 skill 构建的 prompt 启动的——负责搜寻评审其余部分遗漏问题的这道工序,即便运行过,也缺失了 brief 承载的方法。

未检查(工具限制,非阻断):the executable-script lint — .github/workflows/ci.yml: actionlint embedded-shell source mapping is not yet supported — not linted; the executable-script lint — .github/workflows/e2e.yml: actionlint embedded-shell source mapping is not yet supported — not linted; the executable-script lint — .github/workflows/qwen-triage.yml: actionlint embedded-shell source mapping is not yet supported — not linted; the executable-script lint — .github/workflows/release.yml: actionlint embedded-shell source mapping is not yet supported — not linted。

— qwen3.8-max via Qwen Code /review (v0.22.3)

Comment thread packages/cli/src/ui/opentui/mouse-rows.ts
Balachandar Ramakrishnan and others added 28 commits August 29, 2026 18:06
* fix(cli): align provider update model messaging

* test(cli): cover unaffected provider update prompts

* docs(cli): align provider update model guidance

* test(cli): assert provider diff runtime shape

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…8754)

* fix(config): remove dead dynamic command translation setting

* fix(review): keep repository context within file limit

* test(review): pin repository context headroom

* test(config): pin removed setting surfaces

* test(review): make context bound probe robust

* test(review): avoid changed-path exclusion false positive

* test(review): harden manifest policy probes

* test(review): cover all top-level skill sources

* test(review): share repository file walker

* test(review): normalize walked repository paths

* chore(review): drop temporary context CI backport

---------

Co-authored-by: destire-mio <248462155+destire-mio@users.noreply.github.com>
…wenLM#8725)

The `--parallel` flag was a proposal for `npm`, but it wasn't
merged in the end. With version 12 unknown flags are now
errors instead of warnings, so on that version it blocks running
`npm run test` and similar tasks.
* fix(cli): handle unverifiable Windows file identities

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

* fix(core): pin getWorkingDir() assertions to the raw stored cwd

Config resolves targetDir in the constructor but stores cwd verbatim,
so getWorkingDir() returns it unresolved. The rewritten assertions
compared against path.resolve('/tmp'), which holds on POSIX but is
drive-qualified on Windows (C:\tmp), re-breaking both rebinds tests on
the windows-latest lane this PR restores. Restore toBe('/tmp') and
comment the getter-contract asymmetry.

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

* fix(cli): pin the backfill isolation oracle to the exact guard message

expect.any(String) also matches an empty or unrelated error, so the
closed-generation isolation path could silently stop being exercised.
Pin WorkspaceGenerationClosedError's exact message instead.

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

* fix(cli): treat symlink-to-directory ancestors as provably absent

isPathProvablyAbsent's ancestor walk used lstatSync(...).isDirectory(),
which is false for a symlink resolving to a directory, so a genuinely
absent path under a symlinked intermediate was reported "not provably
absent" and forced a full re-review every local round. Probe the
ancestor with statSync so resolved directories are traversable; a
symlink to a file still refuses, and a broken link throws ENOENT and
the walk continues. Document the fail-closed contract on the helper
and add real-fs tests for the directory, file-component, and symlink
shapes.

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

* test(cli): pin isPathProvablyAbsent's Windows-shaped arms with a spied lstatSync

On POSIX a regular-file intermediate raises ENOTDIR at the leaf, so the
ancestor walk added for Windows never executes there: a future
simplification back to "leaf ENOENT ⇒ absent" would pass every real-fs
test while restoring the R19-3 misclassification. Add platform-
independent arms that spy lstatSync into the Windows shape — ENOENT
leaf with a regular-file ancestor stays unmeasurable (false), ENOENT
leaf under a directory ancestor is genuine absence (true), and a
non-ENOENT leaf error is never absence. Restore the fail-closed
pointers the deleted R19-2/R19-3 call-site comments carried.

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

* fix(cli): tighten the review same-file inode predicate to safe-positive values

same-file.ts used core's canonical hasVerifiableInode (Number(ino) !== 0),
but Windows surfaces 64-bit NTFS file indices rounded at the JS boundary:
two distinct files whose indices land in one double-rounding bucket
compare equal, so isSameFile equated them and the anti-clobber guards
findings/save-artifact/repo-context spuriously refused non-colliding
paths. Restate Number.isSafeInteger(ino) && ino > 0 locally instead of
tightening core, whose looser predicate also gates
assertVerifiableTranscriptIdentity on bigint transcript inodes. Extend
the volume-pose harness with a rounded-inode shape: distinct files stay
distinct, one file under two spellings stays one.

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

* refactor(cli): share one inode-verifiability predicate across identity checks

The safe-positive inode semantics this PR establishes (verifiable
predicate, normalize to 0, verifiability-parity compare) were pasted
into standalone-deletion-journal.ts and acpAgent.ts while both already
import conversation-directory-identity.js — a lockstep edit the next
tightening would have to repeat in three places, with one missed copy
enough to let one identity check accept what another rejects. Export
hasVerifiableInode and normalizedInode from the cli-local module and
import them at both call sites; no core barrel import, so the serve
bundle closure is unchanged.

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

* test(cli): cover the unverifiable-inode branch of managed-directory identity

managedConversationExpectation builds expectations from real fs stats,
always safe-positive on CI platforms, so the new degradation branch in
assertManagedConversationDirectoryIdentity never executed — a mutant
that unconditionally claims inode verifiability survives every test yet
reintroduces the 'standalone working directory identity is compromised'
rejection on Windows volumes with file IDs beyond MAX_SAFE_INTEGER.
Mirror the conversation-directory-identity simulation: spy fs.lstat to
report ino 0 and Number.MAX_SAFE_INTEGER + 1, and assert sessionCd
resolves with the inode-0 expectation instead of throwing.

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

* test(cli): import the Stats type from node:fs in the acp agent test

node:fs/promises re-exports no Stats member, so the degradation test's
cast must use the node:fs type directly to keep tsc --noEmit green.

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

* perf(cli): share absence-probe ancestor results across one enumeration

isPathProvablyAbsent walked the same missing ancestor chain to the repo
root once per path. In a sparse-checkout repo every out-of-cone tracked
path is absent with unmaterialized ancestors, so the walk multiplied
the metadata calls by depth+1 on each invisibleTrackedPaths enumeration
— the slowest call on Windows, the platform this series restores (R1-6).
Thread an optional per-enumeration memo of actually-probed ancestor
verdicts through the walk and seed one in the enumeration; the leaf
lstat stays per-path, ENOENT-only fail-closed semantics unchanged. Two
spied-statSync tests pin the one-probe-per-ancestor count and the
memoized non-ENOENT refusal.

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

* fix(cli): share the absence-probe memo across the vanished-path enumeration

`vanishedStillOnDisk` called `isPathProvablyAbsent` without the
per-enumeration `ancestorProbes` memo this PR introduces, while the
sibling enumeration `invisibleTrackedPaths` passes one. A subtree
dropped between rounds (a bulk deletion committed, a branch switch)
sends every vanished path down the same missing ancestor chain:
K×(d+1) synchronous statSync calls where K+O(chain) would suffice —
the multiplied walk this diff introduced at that call site. Pass a
fresh per-invocation memo, mirroring `invisibleTrackedPaths`, and pin
it with an end-to-end capture test counting the ancestor probes (red
without the memo argument).

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

* refactor(cli): close the conversation-identity predicate restatements (R1-8)

The module comment claimed the verifiability predicate was the shared
semantics for EVERY conversation-identity check ("import it, do not
restate it"), while four restatements stood in the tree. Consolidate
what can be consolidated without moving a semantic boundary:

- export `isSameDirectoryIdentity` and replace acpAgent.ts's textually
  identical `isSameManagedDirectoryIdentity` with it (three call
  sites);
- same-file.ts now imports `hasVerifiableInode` from the identity
  module instead of keeping a fourth verbatim copy;
- narrow the module comment to the consumers that actually import it
  and document the two deliberate local restatements that remain:
  `syncStandaloneRoot`'s inline composite around the open handle, and
  `hasExpectedManagedDirectoryIdentity`, whose expectation side must
  keep deriving verifiability from `inode !== 0` because the wire
  payload carries no `inodeVerifiable` field.

Core's looser canonical predicate stays untouched.

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Use the shared path containment helper so workspace-local directories such as ..build are not treated as escapes while real parent traversal remains rejected.

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

* fix(cli): reject symlinked screenshot paths on win32 in capture_screen_context

Windows silently ignores O_NOFOLLOW, so the symlink guard in readPrivatePng was a no-op on win32: a Host returning a symbolic link could have its target read as a screenshot, bypassing the private-directory containment check. Probe the path with lstat and reject symlinks (including NTFS junctions) before opening; POSIX keeps O_NOFOLLOW as a TOCTOU backstop.

Before: 'rejects a symlink and deletes only the Host-provided link' failed on Windows (tool read through the link). After: rejected with an explicit error; only the link is removed and the target stays intact. Part of QwenLM#9481 (cluster 3).

* test(cli): pin the dedicated symlink error message in capture_screen_context

The lstat guard added for win32 had no test that fails when it is removed: on Windows dropping the guard makes the tool read through the link, but on POSIX CI O_NOFOLLOW still rejects the link with a generic ELOOP error, so the regression would be invisible there. Assert the exact 'Host returned a symbolic link screenshot path.' message so both removal paths turn the suite red. Addresses review finding R1-1 (Suggestion).

* chore: re-trigger automatic review

---------

Co-authored-by: zhou2024NAU <zhou2024NAU@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
QwenLM#8702)

* docs(users): add evidence-based-conclusions recipe to common workflows

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

* docs(users): replace scope table with link to memory.md per review feedback

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…enLM#9862)

* fix(acp): route-scope the session token-limit cache in Session.ts

The ACP Session keeps a private `lastPromptTokenCount` fed from streamed
`usageMetadata`, reset only when the chat instance changes
(#syncPromptTokenCountWithCurrentChat). ACP model switches
(unstable_setSessionModel -> setModel -> config.switchModel) rebuild the
content generator but keep the same GeminiChat, so a count recorded on the
previous route survived and anchored the session-token-limit gate for the new
route: any modelOverride send (compression skipped) or any send whose
compression attempt throws reached #getPostCompressionTokenCount(null) with
the stale pre-switch count and was wrongly dropped with
SessionTokenLimitExceeded / stopReason 'max_tokens'.

Attribute the cached count to the route that produced it
(Config.getModelRouteIdentity) and invalidate it on a route change, mirroring
the route-scoping QwenLM#9506 applied to the GeminiChat counts. Same-route counting
and the chat-instance reset are unchanged.

Fixes QwenLM#9529

* fix(cli): retain acp token counts per route

* fix(cli): retain token counts for request route

* test(acp): pin override-route token recording for the QwenLM#9529 gate

Add a QwenLM#9529 regression test that drives a route override through the
full-turn vision selector (fullTurnModelOverride): the first override
send streams usage metadata over the session token limit, and a second
same-override send whose compression falls back to the cache must then
resolve max_tokens — proving the first count was recorded under the
override route key, not the active route's. Reverting the record site
to the default route key makes the test fail.

Also make the hoisted requestRouteKey initialization in
#executePromptInner and #runStopContinuation use optional chaining
(this.config.getModelRouteIdentity?.(...) ?? ''), matching the
#currentRouteKey convention for partial Config mocks; the unguarded
call threw for every prompt in the ~340 Session tests whose mock
config does not define getModelRouteIdentity.

* fix(cli): bound acp route token cache

* fix(cli): cover acp route token eviction

* fix(acp): drop dead request route key initializers in the send loops

The hoisted requestRouteKey initializer in #executePromptInner and
#runStopContinuation computed a route identity that was discarded on
every turn: the null-stream paths return before any record site, and
every path that reaches a record site first assigns requestRouteKey
from the send result. Replace both with a plain empty initializer.

* test(acp): factor the QwenLM#9529 over-limit usage stream setup into a helper

The ~25-line mock setup that streams a 101-token usage metadata chunk on
the first send and an empty stream on the second was pasted verbatim in
eight QwenLM#9529 session-token-limit tests. Extract it into
createOverLimitUsageSendStream next to the existing stream helpers and
migrate all eight copies.

* test(acp): cover route count eviction in the session token cache

The evict-oldest branch in #setLastPromptTokenCount had no coverage:
existing tests exercise at most three route identities, so deleting the
eviction block, flipping the size comparison, or evicting the newest
entry all survived silently. Drive nine distinct route identities (one
past MAX_RETAINED_SESSION_ROUTE_COUNTS) through session.prompt, then
assert the evicted oldest route reads back no cached count (its send
goes out) while a retained route still trips the gate.

* test(acp): pin the stop-continuation token record route scope

The existing override-route recording test only exercises the primary
prompt record site in #executePromptInner; the only Stop-hook gate test
drops the continuation send before streaming and never mocks
getModelRouteIdentity, so the #runStopContinuation record site was
unpinned. Drive a Stop-hook continuation whose send streams over-limit
usage under a \0 exact-route override, then assert a second
same-override send trips the gate from the cached count — reverting the
continuation record site to the default route key makes the test fail.

* test(acp): factor the QwenLM#9529 vision-override mock setup into a helper

* fix(acp): invalidate the session token cache on every compression rewrite

The route-keyed fallback cache was only cleared on a chat-instance change and re-stamped by the pre-send compression hook, so compressions inside GeminiChat.sendMessageStream (hard-tier rescue, reactive overflow — surfaced as StreamEventType.COMPRESSED, which the session loops ignored) left it holding pre-compression counts sized against destroyed history. A returning route's send could then be false-dropped with 'Session token limit exceeded' when tryCompressChat failed.

Handle StreamEventType.COMPRESSED in all four session send loops and clear every retained route count on any COMPRESSED result (pre-send or in-send), re-stamping the fresh count under the request route and the active route when they differ — mirroring GeminiChat clearing its keyed counts in the COMPRESSED branch of tryCompress. Move the pre-send record after request-route resolution so the invalidation keys correctly.

Update the zero-newTokenCount COMPRESSED test to pin the corrected semantics: after a successful rewrite the pre-compression count must not gate the send (owner-side parity).

* fix(acp): re-check abort after the route-key await (QwenLM#9529)

* fix(acp): key cron and background-notification usage records by the request route (QwenLM#9529)

The cron/loop-tick and background-notification send loops captured the request route key and threaded it into the COMPRESSED handler, but their post-stream usage record still called #recordPromptTokenCount(usageMetadata), whose default route key is the record-time active route. A model switch landing between request and record stored the outgoing route's API-reported count under the incoming route's key, so the next new-route send whose pre-send compression failed was false-dropped with 'Session token limit exceeded' (and, on the cron path, could permanently disable cron via #stopCronAfterTokenLimit).

Pass the captured requestRouteKey into the usage record at both call sites, matching the interactive prompt loops. Add collocated tests pinning that each loop records usage under the request route even when the route switches mid-stream.

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(channel): preserve source metadata on resume

Ensure channel workers stamp source attribution when resuming routed sessions so legacy channel conversations are grouped under Channels instead of Tasks.

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

* fix(channel): carry source metadata through restore

Propagate channel source attribution through the SDK restore transport and daemon resume route so resumed legacy channel sessions classify correctly.

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

* fix(channel): persist restore source metadata

Ensure restored channel sessions carry source attribution through the SDK, daemon route, and ACP bridge without overwriting existing persisted attribution.

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

* fix(channel): guard internal restore source metadata

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

* fix(channel): cover restore source review cases

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

* fix(channel): forward restore source type to daemon bridge

Keep channel restore attribution complete when the bridge creates daemon sessions so source ids are not sent without their source type.

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

* fix(channel): cover restore source metadata regressions

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

* fix(session): harden restore source attribution

Reject restores that lose their channel while source metadata is persisted, and make appended source records discoverable after restart.

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

* fix(session): keep source metadata discoverable

Re-anchor durable source attribution as transcripts grow and remove the unused channel source-type option.

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>
Let DeepSeek reasoning models use their model default temperature so deterministic sampling does not amplify repeated thinking loops.

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

Co-authored-by: zhangyu.34 <zhangyu.34@bytedance.com>
* fix(core): disambiguate send_message destinations

* test(core): cover task miss without active team

* test(core): cover teammate hint display

* test(core): cover no-hint display paths

* fix(core): surface teammate hints in task errors
…le-task end (QwenLM#9638)

* fix(cli): deliver teammate messages at tool-round boundaries, not whole-task end

Teammate→leader messages were only drained into the leader's session
when `streamingState === Idle`. During a long multi-round agentic task
`streamingState` never reaches Idle between rounds (tool calls are
continuously scheduled/executing or terminal-but-unsubmitted), so
queued teammate messages waited for the entire task — minutes — even
though the tool description promises delivery when the current turn
ends (QwenLM#8172).

Drain `teammateQueueRef` at the tool-round boundary in
`handleCompletedTools`, appending the envelopes after the
tool-response parts of the next `SendMessageType.ToolResult`
submission — the same mechanism and ordering already used for steer
messages at that exact site (tool_result blocks lead the user
message). The existing Idle drain stays as the fallback for turns
that end without another tool round.

Race/loss safety: the drain is skipped on cancelled boundaries and
generation-change-surviving continuations (mirroring steer), and the
batch is restored (idempotently) on cancel/preempt/admission/delivery
failure so messages are never lost or double-delivered. The
`isSubmittingQueryRef` guard (QwenLM#4844) on the Idle path is untouched.

* fix(cli): settle boundary-drained teammate envelopes by acceptance, not failure

Review round on QwenLM#9638 found two real loss/duplication edges in the
tool-round boundary teammate drain, plus test/observability gaps:

- A drained envelope was delivered TWICE when the boundary submission
  failed after being accepted (user cancel mid-stream, terminal API
  error): GeminiChat pushes the submission's user content before any
  model attempt, yet onDeliveryFailed unconditionally requeued the
  envelope and the Idle drain resubmitted it.
- A blocking UserPromptSubmit hook permanently LOST the envelope:
  ToolResult is not in the hook's exclusion list, the blocked stream
  dispatched onDelivered, and nothing restored the drained queue.
- Boundary deliveries skipped chat recording entirely (recordNotification
  is keyed on Teammate/Notification), so resumed sessions lost both the
  notification item and the envelope from the reconstructed context.

Settle the drained batch by acceptance instead of by failure: snapshot
GeminiChat's user-content push counter (the same signal
settleSteerInput uses in client.ts) before the submission; on failure,
requeue only when the counter did not advance, and when it did, record
the delivery via recordNotification instead of redelivering. Count a
UserPromptSubmitBlocked stream as a delivery failure so blocked
submissions restore their drained payloads.

Also extract the drain protocol (splice + display marking + idempotent
restore) into one drainTeammateQueue helper shared by the boundary and
Idle paths, add debug logs at the drain/record/restore transitions, and
shrink the QwenLM#8172 test harness to a wrapper over renderTestHook. New
regression tests pin the no-duplicate, hook-block restore, recording,
and survivesGenerationChange-skip behaviors (mutation-checked).

* fix(cli): isolate teammate delivery settlement

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

* fix(core): settle steer carriers by the actual push, not the hook window

The attached steer-input carrier (which the QwenLM#8172 teammate boundary
settlement rides) was decided from a push-counter snapshot taken at
sendMessageStream generator entry, before the awaited UserPromptSubmit
hook. ToolResult submissions are not hook-exempt, so a concurrent
submission admitted during the hook await could push its own content
into the global counter and supply the observed push for a send that
never pushed: a hook-blocked or cancelled round settled as accepted,
journaling a delivery that never happened while the drained teammate
envelope was never requeued — silent message loss.

- Blocked sends and hook failures exit before the settlement
  try/finally and provably never pushed: restore the carrier
  unconditionally instead of comparing the counter.
- Re-snapshot the counter immediately before `turn.run` (after the
  hook await) so sends that reach the push compare against the
  tightest window; exits before `turn.run` restore unconditionally.
- Drop the dead push-counter fixture from useGeminiStream.test.tsx
  (nothing under test reads it at this head), correct the settlement
  wrapper comment, and pin the Idle teammate drain's goal-claim
  deferral restore path plus its exactly-once redelivery.

* fix(cli): strip restored teammate envelopes from the Ctrl+Y retry payload

When a boundary submission fails before the history push (e.g. a UserPromptSubmit hook that throws on the ToolResult prompt), the drained teammate batch is restored to the queue, but the same envelopes stay baked into lastPromptRef. A Ctrl+Y retry then re-sends them while the queue still holds them, so the leader receives the report twice (retry + Idle drain). Strip the restored envelopes from the retry payload so the queue redelivery is the single source.

* refactor(core): fold restoreSteerInput into settleSteerInput

restoreSteerInput duplicated settleSteerInput's idempotence guard, try/catch, and failure warning — only the decision differed. settleSteerInput now takes an optional pushCountBefore: undefined marks a send that provably never pushed and restores unconditionally. Keeps the 'settle each carrier exactly once' invariant in one closure.

* fix(core): settle attached carriers by the push-site snapshot

GeminiChat now publishes the user-content push counter on the request
array immediately before pushing it into history, and GeminiClient
settles the attached steer/teammate carrier against that push-site
snapshot instead of a client-side one: a snapshot taken before turn.run
still covers the send-lock and tryCompress awaits ahead of the push,
where a concurrently admitted send (/btw) can push and supply the
counter growth that reads as acceptance for a send that then exits
before its own push (silent teammate loss plus a false delivery
journal). A send that exits before the publish never pushed and
restores unconditionally.

Also settle the attached carrier on the Goal turn admission failure
path, which rethrows before the settlement try/finally and would
otherwise leak the carrier (drained messages neither delivered nor
requeued) for future attachers without their own onDeliveryFailed
fallback.

* test(cli): unify the teammate settlement shim with the real client contract

The Ctrl+Y test rebuilt ~40 lines of the renderBusyMultiRoundTask harness
inline because the shared shim settled in a finally (a throwing stream
accepted) while the real GeminiClient restores on any pre-push exit. Fold
the inline harness back into the shared one and align the single shim with
the real contract: blocked sends and pre-push throws restore, while a push
that landed (first event observed / completed stream) accepts — including
mid-stream failures and consumer abandonment after the first event.

* test(cli): pin the trailing-match guard on the teammate retry strip

The guard that keeps settleDrainedTeammates from stripping the Ctrl+Y
retry payload unless its trailing entries match the restored batch is
load-bearing but pinned by no test: with an unconditional strip, a
goal-preempt restore firing before submitQuery stores the round's own
payload truncates the PREVIOUS turn's retry payload. Regression test:
goal-owned tool round, teammate queued, goal controller aborted
mid-round before the submission; assert the envelope is requeued and a
subsequent retryLastPrompt() re-sends the previous payload intact.
Verified the test fails with the guard replaced by unconditional strip
(observed payload drops [call-r1, call-r1-extra] -> [call-r1]).

* fix(cli): strip accepted teammate envelopes from the Ctrl+Y retry payload

The accept branch of settleDrainedTeammates journaled the delivery but
left the envelope parts in lastPromptRef, so a Ctrl+Y retry after an
accepted-but-failed tool round resubmitted the already-delivered
envelope to the leader. Apply the same trailing-match strip on accept;
journaling semantics unchanged.

* fix(cli): preserve accepted teammate envelopes across the Ctrl+Y retry orphan pop

Acceptance settles on the push, but an accepted round can still fail terminally before any content (a 503 after exhausted retries). The pushed entry is then the trailing orphan the Retry path pops before re-pushing the stored payload, and the landing push suppresses restoreStrippedRetryEntries — so the envelope stripped from lastPromptRef on accept would silently vanish while the delivery journal claims delivered. Record the stripped parts as retry debt and re-attach them in retryLastPrompt exactly when the orphan pop is about to drop them (trailing-match against the popped region), keeping the payload stripped when the entry is not orphaned so accepted-then-failed-mid-stream retries still do not double-deliver.

* fix(cli): harden teammate retry debt against string payloads and identical resends

Addresses both review Criticals on the retry-debt re-attach path:

1. `reattachOrphanedRetryEnvelopes` early-returned for non-array retry
   payloads, discarding the journaled debt unexamined. Idle Teammate/
   Notification drains and plain prompts store strings in `lastPromptRef`,
   so a Ctrl+Y retry of such a payload lost accepted envelopes to the
   Retry path's orphan pop while the journal claimed delivered. Debt is
   now evaluated for any payload shape; string queries are wrapped into
   a text part only when something is re-attached.

2. Debt re-attach matched by envelope text alone, so a byte-identical
   resend orphaning a younger entry re-attached a debt whose own entry
   still sat mid-history — duplicate delivery. Each debt record now
   captures the pushed history entry's parts as an identity fingerprint
   at accept time (tool-response parts carry unique callIds), and the
   re-attach requires that fingerprint to be a trailing orphan entry.

* fix(cli): harden teammate envelope retry debt across admission, retries, and team swaps

Four Criticals from the round-14 review of the boundary envelope retry
debt mechanism:

1. TeamManager swap leaked in-flight boundary batches: the swap handler
   cleared only the queue, while a batch already drained into a
   tool-round submission survived in the settlement closure and its
   restore requeued it into the NEW team's session. A queue generation
   counter now makes the restore drop the batch and the settlement skip
   journal/debt once a swap moved the generation.

2. Retry debt was one-shot: reattach consumed the debt and nothing
   recorded debt for the retry's own re-pushed entry, so an envelope
   surviving one retry could be permanently popped by a later retry of a
   different payload while the journal claimed delivered. The consumed
   records now transfer into a settlement carrier on the retry's own
   submission: accept records debt for the retry's pushed entry, restore
   re-records the original records (core re-adds popped entries as-is
   when the push never landed).

3. Debt was consumed during argument evaluation before submitQuery's
   admission gate ran, so a lease-rejected Ctrl+Y permanently discarded
   it. retryLastPrompt now bails on isSubmittingQueryRef before
   evaluating the debt (for Retry the gate rejects exactly when the
   lease is held, and the path to the gate is synchronous).

4. Debt was recorded only when the accept-time strip matched
   lastPromptRef, but a concurrent submission admitted during the
   time-to-first-token window overwrites it and the orphan pop drops the
   accepted entry regardless. Debt is now recorded unconditionally on
   accept; the retry-time orphan check still keeps double delivery
   impossible.

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

* test(cli): pin teammate retry-debt admission, retry transfer, and swap guards

Regression pins for the four round-14 Criticals:

- a boundary batch restored after a TeamManager swap is dropped, never
  resubmitted into the new team's session
- an accepted-after-swap batch is neither journaled nor recorded as
  retry debt against the new team
- an envelope re-attached by one retry stays protected when that retry's
  own entry is orphaned by a later different payload (debt transfers
  through the retry's settlement carrier)
- a lease-rejected Ctrl+Y does not discard the debt (no history scan, no
  submission, debt still usable afterwards)
- a concurrent /btw overwriting lastPromptRef before the accept
  settlement no longer suppresses debt recording

Adds a non-awaiting startToolRound helper to the multi-round harness so
tests can hold the boundary submission in flight.

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

* fix(cli): strip accepted retry envelopes from the stored payload in the carrier accept

The retry carrier's accept re-recorded debt but never stripped the
re-attached envelopes back out of lastPromptRef, so each
accept-fail-before-content-Ctrl+Y cycle re-attached the envelopes onto a
base that still carried them, appending one duplicate copy per cycle.
Mirror the boundary settlement and the carrier's own restore: strip the
consumed envelope texts before re-recording debt.

Regression test pinned and mutation-checked: reverting the strip makes
the second Ctrl+Y submit [toolResponses, envelope, envelope].

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

* fix(core): re-add popped retry entries when pre-try catches settle the carrier

The Goal-admission catch settles the attached carrier by restore after
the Retry orphan pop has run but before the only
restoreStrippedRetryEntries call site (inside the settlement
try/finally below), so the popped entries were permanently dropped from
history while the carrier re-recorded debt against entries that no
longer exist. Call restoreStrippedRetryEntries before settling in that
catch, and symmetrically in the hook-failure catch (a no-op today, since
hooks never fire for Retry, the only type that populates the entries).

Regression test pinned and mutation-checked: without the re-add the
Goal-admission rejection drops the orphaned entry from history.

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

* fix(core): restore retry entries per send

* test(core): publish push snapshot in retry-restore mocks

The two retry-restore tests anchored the old global push-counter gate
and never published userContentPushSnapshotKey from their mocked turn,
so the per-send snapshot gate saw "no snapshot, never pushed" and
restored unconditionally. Mirror the sibling tests' GeminiChat contract
miniature: publish the counter on the request immediately before the
simulated push, after the simulated compression in the shrink test.

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibaba-inc.com>
* fix(cli): optimize repeated inline image rendering

* fix(cli): make inline image guard type-safe

---------

Co-authored-by: nothing <nothing@U-DQY4PXFJ-0222.local>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
…ifier (QwenLM#10352)

* feat(core): forward bounded MCP tool arguments to the AUTO-mode classifier

DiscoveredMCPTool never overrode toAutoClassifierInput, so every MCP call
reached the classifier as `Tool: mcp__server__tool / Arguments: {}`. Told
to err on the side of blocking, the classifier rejected most of them on
the name alone, which made AUTO mode unusable with MCP and pushed users
toward blanket `mcp__server` allow rules that skip the classifier.

The projection now carries the server name, the server-side tool name,
the server's self-reported annotations, and a bounded copy of the
arguments: 2,000 chars per string, a 16,000-char shared budget, depth and
entry caps, with every cut marked in place and flagged via
`arguments_truncated`. The classifier system prompt explains how to read
the projection — arguments are the evidence for exfiltration and
external-write rules, annotations are unverified, truncation is never a
reason to relax.

`permissions.autoMode.mcp.forwardArguments: false` restores the
name-only projection for deployments whose classifier runs against a
different provider than the main model.

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

* fix(core): make the MCP classifier projection bound hold and keep every key visible

Addresses review round 1 on QwenLM#10352:

- Build projected objects with a null prototype so an argument key named
  `__proto__` stays an own, visible property instead of vanishing through
  the Object.prototype setter (R1-1).
- Cap `server` / `tool` names at 200 chars inside the shared budget, strip
  control characters, and flag cuts with `name_truncated` (R1-2).
- Charge the budget at serialized cost (encoded length plus pretty-print
  line overhead), truncate keys like values, charge every marker, and stop
  container iteration once the budget is spent, so the pretty-printed
  payload the classifier receives stays within the budget plus one marker
  per nesting level (R1-3, R1-4).
- Pick a collision-free key for the remainder marker (R1-6).
- Unify marker forms to `…[truncated N chars]` / `[omitted: …]` and say so
  in the classifier prompt and docs (R1-5).
- Update the base-class `toAutoClassifierInput` docstring to mention the
  MCP override (R1-7).
- Bound rendered historical actions in the classifier transcript: 4,000
  chars each, 40,000 in aggregate, newest kept first, older ones reduced to
  their tool name plus an omission marker (R1-8).

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

* fix(core): announce and pin every MCP annotation key the projection forwards

`ANNOTATION_KEYS` forwards four keys, but the classifier system prompt
and the auto-mode doc enumerated only three: a server could assert
`idempotentHint` and the classifier would see a key the prompt never
marked as self-reported and unverified, right beside the rule that
annotations never justify allowing an action on their own.

Nothing pinned that key either — removing `'idempotentHint'` from
`ANNOTATION_KEYS` left the whole suite green. The projection test now
passes all four keys and asserts all four echo back through the
exact-match `toEqual`, and a new sibling test keeps the boolean-only
filter live (a non-boolean `idempotentHint` must not reach the prompt),
which the widened test would otherwise have stopped covering.

Verified by mutation: dropping `'idempotentHint'` from `ANNOTATION_KEYS`
reds the projection test, dropping it from the prompt enumeration reds
the `MCP guidance` test, and relaxing the boolean filter to a
`!== undefined` check reds the new filter test.

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

* test(core): pin the classifier prompt to the exported annotation key list

The four hand-copied `expect(prompt).toMatch(/xHint/)` assertions duplicated
`ANNOTATION_KEYS`, so they only caught a key *removed* from the projection.
A key added to `ANNOTATION_KEYS` was forwarded by `projectAnnotations`
immediately while the prompt never named it, and the suite stayed green --
the classifier would then receive an annotation key the prompt never marked
as unverified. This is the drift that had to be repaired by hand for
`idempotentHint` one commit ago.

Export the list and iterate it so the guard works in both directions.

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

* fix(core): sanitize classifier projection separators

* fix(core): fail closed when an MCP call's tool has left the registry

Two gaps the maintainer verification found in the classifier projection.

The `permissions.autoMode.mcp.forwardArguments` opt-out lives on the tool
object, so a history entry whose MCP server was removed from settings (or
a session resumed without it) had nothing left to express it:
`projectFunctionArgs` fell back to the raw arguments and forwarded a prior
call's payload — secrets included — into the classifier prompt unbounded.
An `mcp__*` name the registry cannot resolve now projects to the same `{}`
an opted-out MCP tool produces, as does a resolved MCP tool whose
projection threw: for MCP arguments the fallback must not be the unbounded
one. Unknown non-MCP tools keep passing their args through.

A non-object payload (an array, a bare string) projected to `{}` with no
`arguments_truncated` flag, so dropped content read as a call that
genuinely had no arguments — the one place the module broke its own
"omitted content is never presented as absent" invariant. Absent params
stay unflagged; anything else is marked.

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

---------

Co-authored-by: qqqys <qys-us2@outlook.com>
Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
…wenLM#10042)

* fix(serve): prefer a usable issuer over an expired same-subject twin

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

* fix(serve): judge issuer preference and path validity at one clock

* test(serve): pin the validTo boundary and harden the one-clock witness (QwenLM#10042)

* test(serve): fail loudly on a reordered renewed-root fixture (QwenLM#10042)

---------

Co-authored-by: qqqys <qys-us2@outlook.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
* ci: isolate agent workflows from CI runners

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

* fix(ci): align dedicated agent runner safeguards

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(core): preserve ownership during session cleanup

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

* codex: fix CI failure on PR QwenLM#10300

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

* codex: address PR review feedback (QwenLM#10300)

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

* codex: address PR review feedback (QwenLM#10300)

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

* codex: address PR review feedback (QwenLM#10300)

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

* codex: address PR review feedback (QwenLM#10300)

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

* codex: address PR review feedback (QwenLM#10300)

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

* codex: address PR review feedback (QwenLM#10300)

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

* codex: address PR review feedback (QwenLM#10300)

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

* codex: address PR review feedback (QwenLM#10300)

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

* codex: address PR review feedback (QwenLM#10300)

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

* codex: address PR review feedback (QwenLM#10300)

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(serve): hot-reload runtime model providers

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

* fix(daemon): address runtime provider sync review feedback

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

* test(daemon): strengthen provider sync coverage

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

* fix(daemon): harden provider runtime synchronization

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

* test(cli): update worktree ACP provider reload mocks

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

* codex: address PR review feedback (QwenLM#10269)

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

* codex: address PR review feedback (QwenLM#10269)

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

* codex: address PR review feedback (QwenLM#10269)

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

* fix(daemon): harden provider runtime synchronization

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

* fix(serve): synchronize user providers across runtimes

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

* fix(web-shell): update daemon SDK mocks after cutover

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

* codex: address PR review feedback (QwenLM#10269)

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* feat(serve): trust tokenless loopback operator access

Allow the tokenless primary loopback listener to use strict operator APIs, explicitly enabled session shell, Local Control pairing material, and Web Shell Channel controls while preserving hardened and LAN authentication boundaries.

Closes QwenLM#10401

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

* codex: address PR review feedback (QwenLM#10403)

Resolve localhost once and pin the listener to the resulting literal before deriving trusted-loopback authority. Verify the actual listener address before publishing startup, and document the embedder-owned socket boundary.

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

* codex: address PR review feedback (QwenLM#10403)

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

* codex: address PR review feedback (QwenLM#10403)

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

* codex: address PR review feedback (QwenLM#10403)

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

* codex: address PR review feedback (QwenLM#10403)

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

* codex: address PR review feedback (QwenLM#10403)

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

* codex: address PR review feedback (QwenLM#10403)

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: wenshao <shaojin.wensj@alibaba-inc.com>
…nLM#10394)

* ci: gate heavy jobs on a disk floor and persist pressure samples

The in-repo slice of QwenLM#10035: fail fast on a saturated self-hosted host
before npm ci instead of dying on ENOSPC mid-run, and keep the
disk-pressure timeline from a failed run as an artifact so the peak can
be correlated with the job and runner after cleanup reclaims the host.

* ci: single-quote the failure() condition to satisfy yamllint quoted-strings

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

* ci: keep TMPDIR routing block byte-identical across test legs

The sample-file setup landed inside the routing block that
no-ak-integration-ci pins identical across test/test_macos/test_windows,
breaking the identity assertion. Move the DISK_SAMPLES definition and
DISKCONTEXT header ahead of the routing block; the DFSAMPLE lines still
carry the routed tmpdir.

* fix(ci): validate disk floor overrides

* fix(ci): reject oversized disk floor overrides

Keep the disk-floor shell comparisons inside bash's signed integer range by rejecting numeric overrides that cannot be compared safely. Add the existing helper test coverage for the overflow boundary.\n\nCo-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(ci): pin the disk-floor overflow guard boundaries

Pin the three behaviours of validate_floor_override that the current
suite leaves unpinned, each verified to catch its mutant:

- INT64_MAX is a legal floor and must reach the disk comparison
  (tightening the length check to -gt 18 now goes red).
- Zero-padded overrides padded past 19 raw characters are normalized
  and accepted (removing the leading-zero strip now goes red).
- 20-digit values are rejected by the length branch, which the
  existing 19-digit case never exercises (widening to -gt 20 now
  goes red).

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

* refactor(ci): simplify disk floor validation and sampling

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

* fix(ci): preserve the TMPDIR sampler sentinel

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

* docs(ci): clarify disk floor gate placement

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

* test(web-shell): mock the relocated daemon SDK

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

---------

Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* ci: update qwen on third Hong Kong ECS host

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

* ci: allowlist Hong Kong updater labels

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

* test(ci): isolate OSS publisher fixture module mode

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

* test(web-shell): mock the current daemon provider in boot test

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* feat(channels): attribute named session output

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

* codex: address PR review feedback (QwenLM#10420)

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

* codex: fix CI failure on PR QwenLM#10420

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

* codex: address PR review feedback (QwenLM#10420)

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

* codex: address PR review feedback (QwenLM#10420)

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

* codex: address PR review feedback (QwenLM#10420)

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

* codex: address PR review feedback (QwenLM#10420)

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

* codex: address PR review feedback (QwenLM#10420)

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
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.