Skip to content

feat(managed-agent): Spring control plane and dual-path WebShell - #5

Open
doudouOUC wants to merge 16 commits into
cursor/upstream-main-3df3from
cursor/managed-agent-spring-webshell-3df3
Open

doudouOUC wants to merge 16 commits into
cursor/upstream-main-3df3from
cursor/managed-agent-spring-webshell-3df3

Conversation

@doudouOUC

Copy link
Copy Markdown
Owner

What this PR does

This is one review block split out of preview QwenLM#12358, rebased onto current QwenLM main (90232f0eb, published here as cursor/upstream-main-3df3). It adds the standalone Spring control plane (packages/sdk-java/managed-agent-server) and the WebShell Java Managed panel, while leaving legacy daemon sessions in place.

The server embeds the merged Runtime Broker. A new private HTTP listener speaks acquire, control, release, create, start, cancel, and resolve. Create and prepare both call the merged createExecution path, which records the call and drives dispatch. Resolve calls reconcileExecution and does not treat the caller's resolution label as a result. Static placements return a fixed lease. Local-process placements launch the merged stdin attestation worker.

The two adapter commits already opened as #3 and #4 are included here because the server cannot construct the merged broker without them.

Why it's needed

Issue QwenLM#12380 asks for a Hosted Harness control plane in front of qwen serve, with one Session ID and a post-commit event path to the browser. Reviewing that surface inside the 500-file preview is not practical. This block is the Java control plane plus the dual-path WebShell.

Reviewer Test Plan

How to verify

From packages/sdk-java, install qwencode and runtime-broker with -Dgpg.skip=true, then in managed-agent-server run mvn -Dgpg.skip=true test checkstyle:check. Expect the H2 integration tests and the embedded-broker listener test to pass. Checkstyle uses packages/sdk-java/qwencode/checkstyle.xml.

WebShell unit tests were not run in this environment because workspace node_modules is not installed.

Evidence (Before & After)

N/A for screenshots. Linux JDK 21, Maven 3.8.7: managed-agent-server mvn test checkstyle:check passed after the shared session-store fixture was added. Broker HttpRuntimeTransportTest and LocalProcessRuntimeProvisionerTest passed on the included adapter commits.

Tested on

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

Environment (optional)

JDK 21.0.10, Maven 3.8.7. mvn install for qwencode needs -Dgpg.skip=true because the gpg plugin has no signing key here.

Risk & Scope

  • Main risk or tradeoff: the embedded broker is adapted to the merged service, not a copy of the preview broker. Harness-level drain only marks the session closed in this process; the merged broker releases one Runtime Session at a time and does not tear the worker down. Kubernetes provisioning is rejected in this slice. A non-empty local-process environment map is rejected because the merged provisioner has no environment parameter. prepare starts dispatch immediately, same as create.
  • Not validated / out of scope: scripts/run-managed-agent-server-e2e.ts is included and is not wired into CI. QwenHostedHarnessConnector.loadOrCreate still loads before create, so a brand-new stored session whose load returns 500 ownership was not verified (instead of 404) never reaches create. /internal/managed-session-store/v1/ is gated by a client-supplied writer token and tenant header, not by a service credential. The Java session store ships in this block because the connector calls it; the TypeScript journal, authority, and failover tests are a separate review block.
  • Breaking changes / migration notes: broker dependency is 0.1.0-alpha, matching main, not the preview's 0.1.2-alpha. This pull request is on doudouOUC/qwen-code and cannot merge into QwenLM/qwen-code directly.

Design notes: docs/design/2026-09-19-managed-agent-spring-server.md and .zh-CN.md, docs/design/2026-09-20-managed-agent-dual-path-web-shell.md and .zh-CN.md, docs/design/2026-09-20-managed-agent-storage-event-architecture.md and .zh-CN.md, docs/design/2026-09-21-managed-session-durable-store.md and .zh-CN.md.

Linked Issues

Related to QwenLM#12380. Split from QwenLM#12358. Does not close either.

中文说明

这是从预览 QwenLM#12358 拆出的一整块评审,基线是当前 QwenLM main(90232f0eb,在本仓库记为 cursor/upstream-main-3df3)。它加入独立的 Spring 控制面和 WebShell 的 Java Managed 面板,原有 daemon 会话仍然保留。

服务内嵌的是已经合并的 Runtime Broker。新增的私有 HTTP 监听覆盖 acquire、control、release、create、start、cancel、resolve。create 和 prepare 都走合并后的 createExecution,会记下调用并开始派发。resolve 调用 reconcileExecution,不会把调用方传来的 resolution 当成结果。static 供给器返回固定租约。local-process 拉起的是合并后的 stdin attestation worker。

#3 和 #4 里的两笔适配提交包含在本块中,否则服务无法按合并后的构造器创建 Broker。

为什么需要

QwenLM#12380 需要 Hosted Harness 控制面位于 qwen serve 之前,使用同一个 Session ID,并在提交后再把事件送到浏览器。整份预览有五百多个文件,不适合一次评审。这一块只看 Java 控制面和双路径 WebShell。

如何验证

在 packages/sdk-java 用 -Dgpg.skip=true 安装 qwencode 和 runtime-broker,然后在 managed-agent-server 执行 mvn -Dgpg.skip=true test checkstyle:check。H2 集成测试和内嵌 Broker 监听测试应当通过。本环境没有安装 workspace 的 node_modules,WebShell 单测没有跑。

风险与范围

嵌入层按合并后的 Broker 改写,没有用预览里那份 Broker 实现覆盖主线。Harness 级 drain 只在本进程把会话标成关闭,不会拆掉 worker。本块拒绝 Kubernetes 供给器。local-process 若配置了额外环境变量会直接拒绝,因为合并后的供给器没有环境参数。prepare 会立刻派发,和 create 相同。

QwenHostedHarnessConnector.loadOrCreate 仍然先 load 再 create。存储开启时,全新会话如果 load 返回 500 ownership was not verified 而不是 404,就不会进入 create。/internal/managed-session-store/v1/ 只校验调用方自己带的 writer token 和租户头,没有服务级凭证。Java 会话存储因为连接器会调用它,所以留在本块;TypeScript 的 journal、authority 和 failover 测试另开一块评审。

本 PR 在 doudouOUC/qwen-code,不能直接合并进 QwenLM/qwen-code。

关联 QwenLM#12380,从 QwenLM#12358 拆出,不关闭这两个单子。

Open in Web Open in Cursor 

wenshao and others added 3 commits September 25, 2026 10:10
…M#12681)

* feat(sdk-java): Add the W0a Managed Workspace binding contract

Add the first W0 slice of the Managed Agent proposal (QwenLM#12380): the
Workspace binding contract, as a framework-neutral Java module with a
TypeScript twin of its directory rule and digest. Nothing is wired yet.

- Workspace Registry record, a tenant-scoped read contract, and an
  immutable snapshot built from deployment configuration, with a
  successor check against dropped Workspaces, lower generations and
  storage swaps without a new generation.
- Actor access (NONE, READ, CREATE) and an explicit-grant policy.
- WorkspaceCatalog lists the Workspaces an actor can read, with a
  default that does not depend on the page, and resolves a Session's
  selection. Missing, foreign and unreadable Workspaces fail alike, and
  nothing falls back to a default or a launch directory.
- The lexical rule and normal form of cwdRelative. The drive-prefix
  check runs on the normal form, so ./C:x cannot turn into C:x.
- ContextBinding and its contextDigest: SHA-256 over length-prefixed
  UTF-8 items with a domain tag, with integers as decimal text.
- Shared fixtures, 70 path cases and 39 binding cases computed by an
  independent implementation, run by both languages.
- Run the new module in the SDK Java workflow's Java 21 jobs.

* refactor(sdk-java): Move the W0a Workspace binding into runtime-broker

QwenLM#12380 settled the placement: the binding is a separate package inside the
existing runtime-broker module, not a new Maven module. Move the code to
com.alibaba.qwen.code.runtimebroker.managedworkspace and drop the module's
pom, CI steps, workflow-guard tests and size-baseline change; the SDK Java
workflow already runs the runtime-broker tests and Checkstyle. The package
name keeps clear of the repository's unanchored `workspace/` ignore rule,
which would silently drop new files. The runtime-broker README and QWEN.md
gain a section on the package.

Tighten the contract where review found gaps:
- resolve raises workspace_not_found for missing, foreign, impossible and
  unreadable Workspaces from one site, and consults the access policy before
  the state of a default Workspace;
- the TypeScript encoder reads each binding field once and rejects a binding
  that is not an object;
- a snapshot rejects a defaults map holding two equal tenant keys.

Pin the rules in both languages with character probes over the whole Basic
Multilingual Plane and the astral blocks that mimic ASCII, and with fixtures
for lone-surrogate orders, dot segments with spaces and violations deep in
long paths. Java tests now also cover content and case-sensitive comparisons,
constructor visibility, significant spaces, locale-independent test data and
the listing's scan bounds. The design document records the decided placement
and moves the boot envelope to its own follow-up change.
…ntials (QwenLM#12680)

* perf(cli): keep running in place when env files only hold model credentials

The env-file relaunch rule from QwenLM#12602 fires for any value that `.env` files
or `settings.env` inject, including the API key that `/auth` stores and the
keys the docs recommend for `~/.qwen/.env`. Those users always paid the
second boot. Providers read credentials and endpoints per request, never
while a module loads, so `hasLoadedEnvironmentValues()` now ignores the
documented per-auth-type variables and the `envKey` of every configured
model provider. Any other injected value still forces a fresh image, and a
provider entry can never exempt a loader or boot-time key.

The DashScope proxy base URL was the one such variable read at module load;
it is now read at call time.

Reported by @wenshao in the QwenLM#12622 review (F2).

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

* feat(scripts): let the startup benchmark load credentials from a file

`--credentials settings|dotenv` moves the stub's key and URL from the shell
into the settings file's `env` block or `~/.qwen/.env`, so the env-file
relaunch rule can be measured. The default stays `shell`.

Claude-Session: https://claude.ai/code/session_01163b7X4Ntw1S2pr9VrtRrs
…QwenLM#12671)

* feat(cli): Declare the v2 execute/status/cancel Managed Runtime contract

Extend the owned managed-runtime route manifest with the three tool
operations and add their shared schema and conformance fixtures. The
wire shape fixes the evidence rules the recovery design demands: every
call is keyed by the original reference (sessionId, promptId, callId,
argsDigest) rather than any Broker-side identifier; status is read-only
and answers unknown with 200 instead of 404 or 500, because a missing
record is never evidence that a call did not run; a settled state is
the only one that may carry a result; and status additionally returns
the Runtime's own lastSequence cursor. Execute accepts up to 256 KiB
of request input, and every operation answers at most 1 MiB.

No handler is mounted yet: per the manifest rule the routes join in the
same line of slices as the execute extraction that implements them. The
TypeScript gate test proves the owned-route gate admits exactly these
paths, and the Java consumer pins the route contract, every outcome
classification, and the closed field sets, so the worker and transport
slices that follow share one reviewed target.

Related to QwenLM#12380.

* fix(runtime-broker): Accept unknown cancellation status

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

* feat(cli): Mount the v2 tool operations on the Managed Runtime worker

The attestation worker now serves execute, status, and cancel beside
attest. Its executor admits exactly the first-slice ordinary tools —
read_file, write_file, edit, and foreground run_shell_command — over a
real Config rooted at the attested workspace cwd, with checkpointing
disabled and no further approval gate, because admission happened on the
Harness side. An in-memory journal answers status and cancel by the
original reference; it is process-local by construction, since the worker
process is the Runtime generation and a restart is a new generation
rather than a continuation.

execute is idempotent by reference.callId: the same identity joins the
in-flight invocation or returns its settled result, and the same callId
with a different digest or payload is a 409 identity conflict. status is
read-only and answers unknown with 200 for a reference the Runtime never
saw. cancel settles a prepared invocation without touching the tool,
aborts an executing one, and a cancel the Runtime honored settles as
cancelled whether the tool surfaces the abort as an error or as an early
result. The worker's HTTP requestTimeout is disabled because an execute
call holds its connection until settlement.

Related to QwenLM#12380.

---------

Co-authored-by: jinye.djy <jinye.djy@U-Y37CF7G3-1943.local>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Shaojin Wen <szujobs@gmail.com>
@doudouOUC
doudouOUC marked this pull request as ready for review September 25, 2026 11:05
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-25T12:28:53.785259Z 352e2ee New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2c68513d3d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +223 to +225
public CompletionStage<Void> acquire(RuntimeLease lease,
RuntimeSession session) {
return unsupportedSessionVerb();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Implement session verbs before exposing the broker route

When the embedded Runtime Broker is enabled, a fresh /tool-sessions:acquire request reaches RuntimeBrokerService.acquire, which invokes this transport's acquire; the implementation always returns a 501 failed future. EmbeddedRuntimeBroker supplies this same transport for both local-process and static provisioners, so the Hosted Harness cannot acquire any Runtime Session and therefore cannot reach the new control or tool-execution flow. The broker-facing transport needs working session semantics rather than unconditional rejection.

AGENTS.md reference: AGENTS.md:L266-L269

Useful? React with 👍 / 👎.

Comment on lines +223 to +229
try {
return load(tenantId, sessionId, false);
} catch (DaemonHttpException error) {
if (error.getStatusCode() != 404) {
throw error;
}
return create(tenantId, sessionId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Handle an uninitialized durable session before loading

When the Managed Session Store is enabled for a brand-new Java session, this probes loadSession before any create request has initialized the durable journal. The current Hosted Harness can report that state as a non-404 ownership-verification failure, but this catch only falls back to create on 404, so the first accepted turn never creates its Harness Session and repeatedly fails dispatch. Treat the store's not-yet-created state as a create path, or use a create-first/idempotent attach flow.

AGENTS.md reference: AGENTS.md:L270-L274

Useful? React with 👍 / 👎.

Comment on lines +62 to +64
HarnessSessionResolver resolver = sessionId -> {
SessionRecord session = store.findSessionById(sessionId)
.orElse(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject non-active sessions in the broker resolver

After Spring restarts, the process-local retired set is empty, while archived and deleted rows remain discoverable here because lookup is by ID alone. An authenticated Harness can therefore warm or acquire a Runtime scope for a persisted session whose lifecycle state is ARCHIVED or DELETED, bypassing the drain decision recorded before the restart. Check the stored session status and fail closed unless it is active.

AGENTS.md reference: AGENTS.md:L270-L274

Useful? React with 👍 / 👎.

…wenLM#12622)

* perf(cli): halve fresh-startup time to typeable and cut RSS by 60%

Measured with a clean-environment benchmark following the approach in
https://claude.dev/blog/how-we-made-claude-ai-faster/ (interleaved A/B,
p75 wall-clock backed by deterministic proxies). Against main, time to a
typeable prompt goes from 2.55 s to 1.18 s, `qwen -p` time to first
request from 2.15 s to 0.94 s, and RSS at the prompt from 574 MB to
231 MB, with no screen change after the prompt becomes usable.

- Skip the supervisor relaunch when it adds no Node flags; restarts
  re-exec in place and updates run in-process (POSIX, plain TUI and -p).
- The bin launcher runs the CLI in-process on POSIX.
- Enable Node's compile cache on the default route.
- Move startup side effects off the critical path: editor probing, the
  IDE process walk outside VS Code, and the npm-based update check.
- Resolve the git branch before the first frame so the footer does not
  reflow the screen after the prompt is usable.
- Bundle with minifyWhitespace; load the review command and non-common
  highlight grammars only when needed.

Experiments, tradeoffs and open decisions are in
docs/design/2026-09-24-fresh-startup-performance.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1XQv6wm9YJ5rSM53eMSRT

* fix(cli): keep global.gc in relaunched sessions

On POSIX the bin launcher now runs the CLI in-process and exposes gc at
runtime, so --expose-gc is no longer in process.execArgv. The supervised
relaunch built its child's flags from execArgv alone, so the session
process lost global.gc in --acp, stream-json, --json-fd, file input,
advanced.autoConfigureMemory, and on Node without process.execve.

Both relaunch paths now take their flags from one helper that adds
--expose-gc when gc is exposed but not on the command line.

* fix(cli): relaunch when env files injected values

QwenLM#12602 re-boots a one-shot run whenever .env or settings.env injected
values, because modules that read the environment at load time (and
Node itself, e.g. NODE_EXTRA_CA_CERTS) only see them in a fresh image.
The in-process path ran ahead of that check and skipped it for both
interactive and -p. Take the in-process path only when no env-file
value was injected; otherwise relaunch as main does.

* fix(cli): keep the spawned CLI under Bun

The Bun standalone flavor (the OpenTUI preview archives) runs the same
bin entry. Bun has no v8.setFlagsFromString, so the in-process path
crashed every non-fast-path command at startup on macOS and Linux.
Under Bun the entry spawns the CLI with --expose-gc, as on Windows.

Also correct the entry's header comment, the relaunch-flags helper's
doc comment and a displaced test comment, and narrow the design doc's
gc claim to the modes that were checked.

* fix(cli): install a deferred update after a clean exit

Without a supervising parent, the startup check fell back to "Run
/update to install the update." for standalone and update-command
installs. Record the request in-process instead: once the session ends
cleanly, install the update and exit with its code, so the launcher's
exit hook reopens the new version as the parent did on main. A second
quit during the install waits instead of cutting it short.

* docs(design): record the fresh-startup decisions

Resolve the open decisions, and describe the env-file relaunch rule,
the Bun launcher path, the restored update-on-exit and the independent
reproductions, in both language versions.

* docs(design): record decision 7 as a manual harness without a CI gate

The follow-up (QwenLM#12674) checks in the benchmark harness for manual runs.
The interactive startup counts stop at the typeable prompt, and
timer-driven work before it shifts with runner speed, so a CI gate
would be flaky.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 352e2ee93e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

() => managedSelectionFromUrl().sessionId,
);
useEffect(() => {
saveManagedSelection(activePanel === 'managed', managedSessionId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve host URL parameters when Managed mode is disabled

When managedAgentProvider is absent, activePanel cannot be managed, so this effect immediately calls saveManagedSelection(false, ...) and deletes any existing managed and managedSession query parameters. Consequently, merely mounting the ordinary or embedded WebShell mutates host-owned navigation even though the Managed feature was not enabled; hosts that use either parameter for their own routing lose that state. Only synchronize these parameters when the Managed provider or Managed navigation contract is active.

Useful? React with 👍 / 👎.

Comment on lines +54 to +56
if (sessionStore.isEnabled() && runtimeWorkspaceId != null
&& !runtimeWorkspaceId.isBlank()
&& !workspaceId.equals(runtimeWorkspaceId)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate the store ID against the derived runtime ID

When the Session Store and a local-process Runtime Broker are both enabled but the broker's workspaceId is blank, this check is skipped even though EmbeddedRuntimeBroker.resolveWorkspaceId subsequently derives a concrete ID from workspaceCwd. A separately configured Store ID can therefore differ from the Runtime scope while startup succeeds, causing the Harness journal and Runtime binding to use different (tenantId, workspaceId, sessionId) identities. Compare against the broker's effective derived ID, not only its raw optional property.

AGENTS.md reference: AGENTS.md:L270-L274

Useful? React with 👍 / 👎.

yiliang114 and others added 12 commits September 25, 2026 12:39
…P mode (QwenLM#9531)

* fix(cli): make the Agent Team teammate tab transcript scrollable in VP mode

In Virtualized History mode (ui.useTerminalBuffer, on by default) the
app owns the whole screen, so there is no terminal-native scrollback.
The main conversation view renders its transcript in a ScrollableList
virtual viewport, but AgentChatContent rendered the teammate tab
transcript through ink's <Static> unconditionally — content that
scrolled off screen could not be reached by Page Up, Page Down, arrow
keys, or the mouse wheel (QwenLM#9507).

Route the teammate transcript through the same ScrollableList viewport
in VP mode: header + committed + live items + spinner live in one
viewport pinned to the tail, with Page Up/Page Down, Shift+arrows,
Ctrl+Home/End and wheel/scrollbar support. The committed/live split is
preserved (items from an executing/confirming tool group onward stay
on the interactive render path so approval dialogs keep receiving
input). The legacy <Static> path for useTerminalBuffer: false is
unchanged — native terminal scrollback already works there.

* fix(cli): preserve teammate transcript selection

* fix(cli): re-measure the agent footer when its height changes

AppContainer's controls-height measure effect tracked only
main-conversation state, so on the agent tab the Completed/Failed
status row, queued messages and input growth never triggered a
re-measure: controlsHeight stayed stale-high and the transcript
viewport pushed the composer and tab bar past the terminal bottom.

AgentComposer now derives a layout key from its height-shifting state
(streaming state, status row, queued messages, input text) and syncs
it to AgentViewContext; the measure effect depends on that key,
mirroring liveAgentPanelLayoutKey from QwenLM#5798.

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

* fix(cli): use renamed RespondingSpinner in agent transcript viewport

Main renamed GeminiRespondingSpinner to RespondingSpinner; the merge
kept the old name at the viewport's spinner item and broke typecheck.

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

* fix(cli): follow RespondingSpinner rename after merging main

Main renamed GeminiRespondingSpinner to RespondingSpinner; update the
teammate-tab spinner usage and its test mock so the merged tree compiles.

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

* test(cli): guard the AgentViewContext layout-key propagation deps (QwenLM#9507)

R3-1: the provider link of the QwenLM#9507 layout-key chain was the only link
without a source guard. Pin agentComposerLayoutKey in the state memo deps
and setAgentComposerLayoutKey in the actions memo deps of
AgentViewContext.tsx by extending app-container-controls-dep.test.ts in
the PR's established guard style. Mutation-verified: dropping either
entry from its deps array (while the object literal keeps it) turns
exactly the matching new test red, and every pre-existing test stays
green.

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

* fix(cli): restore full-detail toggle and mouse actions on teammate VP path

The virtualized render path added for the teammate tab dropped two things
the main conversation view has:

- `fullDetail` was not forwarded to either HistoryItemDisplay site, so the
  app-wide Ctrl+O/Alt+T toggle did nothing for tool groups on this tab
  while truncated rows kept advertising "... +N chars (ctrl+o)".
- ScrollableList arms SGR mouse capture (bypassVpGate), but
  ContentMouseController was never mounted. With tracking armed the
  terminal stops handling the mouse itself, so OSC 8 link clicks and the
  right-click context menu silently did nothing.

Forward `fullDetail` on both VP branches (pending and committed) and mount
ContentMouseController next to TextSelectionController, mirroring
MainContent's VP path. The menu itself is already painted by the shared
ContextMenuOverlay in DefaultAppLayout, which sits above this tab.

Tests pin both: HistoryItemDisplay receives the toggled `fullDetail` on
both branches, and ContentMouseController mounts in VP mode only.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmto4amd1ku

* fix(cli): quiet the teammate VP viewport while the context menu is open

Mounting ContentMouseController made the right-click menu openable on the
teammate tab, but this branch never consulted useContextMenu(), so scroll
keys and wheel ticks kept reaching the viewport underneath the pinned menu
and a press or drag on a menu item anchored or cleared a transcript
selection under the overlay.

Mirror MainContent's VP path: gate ScrollableList's hasFocus on
contextMenuOpen === null, and pass TextSelectionController
eventsPaused={contextMenuOpen !== null} — paused rather than deactivated,
since deactivating clears the selection the menu's Copy Selection offers.
ContentMouseController stays ungated: it owns the open menu's pointer
interaction and its deactivate effect calls closeMenu().

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtq0vr74ni

* fix(cli): clear the painted text selection when its controller unmounts

The highlight range is painted onto the per-stdout frame controller, which
lives outside React, so it outlived the controller that painted it: ink
re-reads it on every serialization pass and stamps the background onto
whatever cells now occupy those coordinates. A selection made in a teammate
transcript therefore survived the tab switch and was re-stamped over rows of
the replacing view the user never selected, and no successor could repair it
-- clearSelection() early-returns while its own selection is empty, which is
what every freshly mounted controller starts with, and the frame-invalidation
callback early-returns on the same condition.

The teardown is unconditional (it goes straight to setSelection(null) rather
than through clearSelection()) and unmount-only: it must never fire on
eventsPaused, which exists to keep the selection that the open context menu's
"Copy Selection" item offers. getBuffer is a useCallback keyed on [stdout], so
the effect is stable and its cleanup runs exactly once, on unmount.

This closes the whole class rather than one route: teammate -> main,
main -> teammate, and agent -> agent (AgentChatView keys its boundary on
agentId) all unmount the surface that owns the selection.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtqfvvmto2

* fix(cli): gate the teammate tab's key consumers on the open context menu (QwenLM#9531)

Mounting ContentMouseController made the right-click menu reachable on the
Agent Team teammate tab, but the tab's other key consumers did not gate on it:
KeypressContext broadcasts to every subscriber and discards return values, so
a key aimed at the menu was also delivered to the composer and the approval
dialog. The composer now goes quiet on up/down/return/escape while the menu is
up (and closes it on any other key, then falls through), and the approval
dialog folds the open menu into the same `isFocused` flag it already gates its
keypress handler and RadioButtonSelect on. Both mirror the main view's
InputPrompt gate.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtyw8o2w11

* fix(cli): keep teammate approvals answerable and gate the last key consumers (QwenLM#9531)

R13-1: on the VP path a teammate's pending ToolConfirmationMessage is a tail item inside the windowed list, and dialogsVisible is derived from main-app dialog state only, so one Page Up unmounted the dialog together with its key handler and left the round blocked with nothing on screen. Pull the tail back into view when an approval arrives, then take the scroll keys away - the gate alone would trap a user who had already scrolled up, with no way back.

R13-2: KeypressContext broadcasts to every subscriber and discards return values, so an open right-click menu cannot consume a key for anyone. Gate the three remaining consumers on the teammate tab (tab bar arrows, the Shift+Tab approval-mode cycler, the Ctrl+F embedded-shell toggle), matching the two already gated in b479224.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtzfj4451s

* fix(cli): keep the deps guard's identifier out of comment prose

The source guard in app-container-controls-dep.test.ts substring-matches
the raw deps-array text, comments included. Naming liveAgentPanelLayoutKey
in this comment meant deleting the real dep entry left the guard green.
Reword so the identifier appears only as the dep entry itself; verified
by mutation (deleting the dep entry now fails the guard).

* fix(cli): drop a queued scroll flush on focus loss and gate the nested answer input

Two review Criticals on the teammate tab's approval surface:

- ScrollableList left a wheel/drag flush armed across a focus loss, so the
  timer fired after whatever took focus had pulled the tail into view and
  scrolled a pending approval dialog back out of the render window, where
  its keys and this list's were already dead.
- AskUserQuestionDialog hard-wired its nested custom-answer TextInput to
  isActive={true}, so an Enter aimed at an open right-click menu still
  submitted the half-typed draft as the answer to the pending question.

* fix(cli): block stale key handlers while context menus open

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

* fix(cli): preserve keys after input dismisses context menu

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

---------

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

* fix(cli): restore ripgrep exec bit on managed npm update activation

npm/pnpm pack preserves the exec bit only for `bin` entries, so the
published tarball ships every vendor/ripgrep/*/rg as 0644, and the
published manifest intentionally carries no postinstall to restore it
(QwenLM#6164 removed that surface). The managed self-update path installed,
validated, and activated such a staging tree without ever touching the
vendored binary modes, so every self-update landed a ripgrep that fails
to spawn with EACCES and silently falls back (QwenLM#12668).

Chmod the staged vendor/ripgrep/*/rg to 0755 after validation and smoke
test, before the activation rename. Best-effort like the source-tree
postinstall: platform dirs without a unix rg are skipped, and any other
chmod failure only logs — the runtime probe still falls back to a system
ripgrep or the JS grep tool, so an update must never fail over this.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-issue-patrol/jmugdo4w20u

* fix(cli): heal the ripgrep payload that activation actually adopts

Review round 1 follow-ups on the managed npm update heal:

- Guard the exec-bit assertion with `process.platform !== 'win32'` instead of
  calling it vacuous there: libuv synthesizes the mode from the read-only
  attribute alone, so a writable regular file stats as 0o666 and the assertion
  cannot pass on Windows. Only the assertion is guarded, so the case still runs
  there and still covers the win32 `rg.exe` ENOENT swallow.
- Heal the version directory that survives the rename collision, and the higher
  version an existing pointer keeps, not only the staged tree. Both exits of
  `activateManagedNpmUpdate` previously left `active.json` naming a payload this
  process never healed, and the collision branch discards the healed one first.
  The heal stays metadata-only, so a live older session importing that directory
  is unaffected.
- Extend the fixture with the `COPYING` entry the published tree really carries
  beside the platform directories, and add a non-ENOENT chmod failure (ELOOP via
  a self-referential symlink) so the skip guard, the ENOENT discrimination and
  the "must not block the update" contract are all pinned.
- Attribute the 0644 normalization to `pnpm pack`, which is what the release
  publishes with (`.github/scripts/run-release-step.sh` has no `npm publish`).

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmugktc5f3t

* refactor(cli): drop unobservable chmod-failure reporting from the ripgrep heal

The `code !== 'ENOENT'` discrimination existed only to decide whether to call
`debugLogger.warn`, and that call cannot reach a sink in the one process that
runs it: `runCliEntry` awaits `installManagedNpmUpdate` and returns before
route dispatch (cli.ts:520-530), so no `Config` is constructed there and
`setDebugLogSession` is never called (config.ts:3104, :5524); `warn` returns
early without a session (debugLogger.ts:296-300).

Collapse it to a bare catch that states why nothing is logged. The
`!entry.isDirectory()` guard goes with it: `readdir` names are all the loop
needs now, and `Dirent.isDirectory()` reports a symlinked platform directory
as false, so the guard would skip a layout the chmod would have healed.

Also drops the test's `vi.hoisted`/`vi.mock` debugLogger spy and its two
`warn` assertions, which asserted the mock rather than product behaviour. The
COPYING fixture entry stays: the real tree carries it, and the heal now
genuinely attempts and swallows `COPYING/rg`.

232 -> 204 added lines. Mutation panel re-run one mutant at a time at this
head, zero survivors: dropping the staging heal reds 2 cases, the
collision-branch heal 1, the activeDir heal 1; making the chmod catch rethrow
reds 4 and the readdir catch rethrow reds 9. 19/19 tests pass, prettier and
eslint clean, no type errors in the changed files.

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

---------

Co-authored-by: yiliang114 <yiliang114@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
… fails (QwenLM#12666)

* fix(cli): notify when the Linux clipboard tool is found but its query fails

On Linux, getLinuxClipboardTool() only probes `command -v wl-paste/xclip`; it never touches the display server. With a stale or dead X server (or a compositor socket that went away) the probe passes while the actual query — xclip -t TARGETS -o or wl-paste --list-types — fails, and the paste was swallowed silently: non-zero exit, spawn error/throw and timeout all collapsed into a bare false, indistinguishable from "no image".

Thread onUnavailable into checkClipboardForImage() and getWlPasteImageTypes() and invoke it at each failure exit (non-zero exit, spawn error, spawn throw, timeout), so the caller's unavailability warning fires. A successful query that finds no image still never notifies, so copying text stays quiet, and the no-tool path is unchanged.

Refs QwenLM#12505 (finding 1 of 3).

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-issue-patrol/jmufyo0er0o

* fix(cli): keep an empty Linux clipboard from claiming the native module is broken

Linux clipboard tools use one non-zero exit code for both "the display
server is dead" and "nothing is on the clipboard", so keying the
unavailability notification on the exit code made the image-paste binding
report that the native clipboard module could not be loaded whenever the
clipboard was simply empty.

Capture fd 2 in checkClipboardForImage() and getWlPasteImageTypes(), the
same way saveFromCommand() already does so a non-zero exit can be
diagnosed rather than merely reported, and skip the notification when
stderr identifies the exit as a benign empty clipboard:

- xclip: "There is no owner for the ... selection" (xcprint.c
  errconvsel(), exit(EXIT_FAILURE) when XGetSelectionOwner() is None)
- wl-paste 2.x: "Nothing is copied" (bail() is fprintf(stderr) + exit(1))
- wl-paste 1.x: "No selection" (same bail() path)

An unclassified non-zero exit still notifies, and the spawn-throw,
spawn-error and timeout notifications are left untouched.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmug5t7mp33

* test(cli): make the Linux clipboard failure paths diagnosable

Review follow-up to the three Suggestions on this PR:

- clipboardUtils.ts: log the exit code (and args) unconditionally when a
  clipboard query exits non-zero, and log both new timeout paths, mirroring
  saveFromCommand in the same file. A tool that fails without writing to
  stderr (xclip exits EXIT_FAILURE silently) notified the user while leaving
  nothing in the debug log to explain why.
- clipboardUtils.ts: bind and log the error in the new catch around
  spawn('wl-paste'). saveFileWithWlPaste calls getWlPasteImageTypes without
  an onUnavailable callback, so a synchronous spawn throw was invisible.
- clipboardUtils.test.ts: give the two inline BMP fixtures the stderr stream
  production now reads. Without it, child.stderr.on threw inside the promise
  executor and both tests passed on saveClipboardImage's catch-all null with
  nothing past the type query executed. They now assert the unlink of the
  derived .bmp path (and .png before .bmp), and a createDebugLogger spy pins
  each new log line.

Logging only: no notify condition, return value or cache semantics change.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmugedurp3j

* fix(cli): match released xclip empty-clipboard wording, latch the notify

Two defects in the Linux clipboard failure classifier, both reported
against d4439fc.

The only xclip marker was 'no owner for the', which comes from errconvsel()
in unreleased xclip git master (xcprint.c:136, called from xclip.c:778).
Released 0.13 -- the version Debian, Ubuntu and Fedora package -- has no
errconvsel() at all: a -t TARGETS -o query on a selection nobody owns takes
the XCLIB_XCOUT_BAD_TARGET branch that has no fallback left, prints "Error:
target TARGETS not available" and returns EXIT_FAILURE (xclip.c:468). No
marker matched that, so a routine empty clipboard fell through to
onUnavailable and rendered the "native clipboard module could not be
loaded / reinstall Qwen Code" notice, which cannot be true on the Linux
branch because it never loads that module. main returned false silently on
this exact input, so it was a regression, and it spent
clipboardUnavailableShownRef -- latched once per session and never reset --
so the genuine dead-X-server failure this PR exists to report could not be
reported again for the rest of that run. Both spellings are now markers,
and the doc comment says which xclip each one belongs to.

The unconditional exit-code log copied saveFromCommand's line but not the
`if (resolved) return;` latch that makes that line safe there. Node delivers
`close` after a spawn `error` (carrying the negated errno) and after a
timeout `kill()` (with code === null), so the close handler re-entered its
non-zero branch for a query that had already been reported: it recorded
`exited with code -2` for a binary that never started, `exited with code
null` immediately after the timeout line, and fired onUnavailable a second
time for one failure. Added the same settle latch to checkClipboardForImage
and to getWlPasteImageTypes, and logged the spawn errno in both error
handlers, since with the fabricated exit code suppressed that is otherwise
the only trace a spawn failure leaves. The failure paths still do not cache:
the latch returns before `cachedWlPasteImageTypes = types`.

The two fixtures now reproduce Node's real event order -- createSpawnErrorChild
emits close(-2) after error, and createHangingChild's kill emits
close(null, 'SIGTERM') -- so the existing toHaveBeenCalledOnce assertions
witness the latch instead of a sequence production never produces.

61 tests pass (60 before, +1 for the released xclip wording). Every new or
strengthened assertion was mutation-checked on its own: removing the marker
reds the new test with "expected spy to not be called at all, but actually
been called 1 times"; removing either function's latch reds exactly its
three witnesses ("expected spy to be called once, but got 2 times" plus the
'exited with code' negative) while the genuine exit-code, empty-clipboard and
dead-display tests stay green. cli typecheck reports no clipboardUtils error.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmugktc5f3t

* test(cli): fold the duplicated timeout cases, correct the marker provenance

Cleanup for the two round-2 Suggestions still open at 99289af. Net -21 lines;
no production behavior change beyond comments.

R2-2: the two `records the timeout when the query never answers` cases re-ran
the exact scenario the two `notifies when the query times out` cases already
cover -- same env stubbing, same probe result, same createHangingChild(), same
call resolving to false -- and differed only in which callback they asserted
on. Each spent a real five seconds against the module-private
PROCESS_TIMEOUT_MS, so the pair cost ten seconds on every local run and every
CI shard that picks up this file. Their witnesses are folded into the two
existing cases, which now assert a superset: the timeout log line, the
`exited with code` negative that catches the fabricated exit code arriving from
kill()'s close(null, 'SIGTERM'), and notify-once. Test count 61 -> 59,
five-second cases 4 -> 2, file duration 22.9s -> 18.0s.

Driving the remaining two with fake timers would remove the last ten seconds,
but that is test infrastructure rather than a defect, so it is left as
follow-up.

R2-3: the BMP witness comment read as confirming the python3 conversion path is
now pinned. It is not. fs.open is unmocked while mkdir is a no-op, so
saveFromCommand fails at its O_EXCL open before spawning, python3 never runs,
and the unlink counted there is the save-failure tail -- which also leaves the
fixture's callCount 2/3 arms unreachable. The comment now says so. Covering the
conversion catch and deleting those dead arms are pre-existing on the base
commit, so they stay follow-up rather than widening this PR.

Also corrects the wl-paste provenance: "Nothing is copied" arrives in
wl-clipboard v2.2.0 (src/wl-paste.c:252), not at the 2.x boundary -- v2.0.0
and v2.1.0 still print "No selection" (src/wl-paste.c:187). Both markers were
already load-bearing, just for a different version reason than stated, which is
the same master-versus-released confusion R2-1 was about. The citation now
lives once on EMPTY_CLIPBOARD_STDERR_MARKERS instead of being restated
verbatim in the test file; the duplication is what let the two copies drift.

Each folded witness was re-mutation-checked individually after the move:
removing the released-xclip marker reds its case with "expected spy to not be
called at all, but actually been called 1 times", and removing either
function's close latch or timer latch reds exactly its case with "expected spy
to be called once, but got 2 times". 59 tests pass; eslint, prettier --check and
cli typecheck report nothing on either changed file.

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

* test(clipboard): pin the four diagnostic logs R3-1 named as unasserted

clipboardUtils.ts logs four diagnostics on the Linux clipboard failure paths
that the new tests executed but never asserted, so reverting
child.on('error', (err) => { to the pre-PR child.on('error', () => { dropped
the binding and the log together and still shipped green.

Those logs are load bearing. The settle latch suppresses the close handler's
fabricated exit code, so the errno is a spawn failure's only trace, and the two
stderr echoes are the only record of the wording isEmptyClipboardError()
classified -- without them a failing query reads just "xclip exited with code 1.
Args: ..." and the classification decision is no longer auditable. Until QwenLM#12504
fixes the onUnavailable text, the debug log is also the only place the real
failure reason is recorded anywhere.

One assertion per site, added to the tests that already reach them, following
the toHaveBeenCalledWith convention the other three logs in this PR already use.
The wl-paste async-error assertion targets .debug, not .error, because the
synchronous-throw arm logs the byte-identical message at the other level and is
pinned separately; the two stderr expectations omit the fixtures' trailing
newline because both production sites call .trim().

Each assertion was mutation-checked individually: deleting or reverting its own
site now reds exactly one test (1 failed | 58) with no collateral, and the
production file is restored byte-identical afterwards. Test-only change -- no
log statement was removed, reworded or consolidated.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmugtdz9149

---------

Co-authored-by: yiliang114 <yiliang114@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…k aggregation (QwenLM#12689)

When multiple PreToolUse hooks match one tool call, the documented
hookSpecificOutput.permissionDecision fell into the generic
"later values win" merge branch, so the last hook in config order
overrode any earlier deny and the tool ran. Rank the field during
aggregation (deny > ask > allow) with the reason following the
winning rank (same-rank reasons concatenate), leaving top-level
decision OR-logic and last-wins for other hookSpecificOutput fields
unchanged.

Fixes QwenLM#12683


Patrol-Run: qwen-issue-patrol/jmugqj3jg10

Co-authored-by: yiliang114 <yiliang114@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Add the contract for W0a-2, the second W0 slice of QwenLM#12380: how a Runtime
Broker and a Managed Runtime worker agree on managed-context/1, boot
document v2 and ready record v2, attestation v3, and the context
installation request with its receipt. Nothing is wired yet; W0c mounts
the routes and writes boot v2.

- Boot v2 carries the Workspace binding and the mount root but no
  Session context, since a Workspace-isolated Runtime is shared. Every
  field that a response repeats is bounded, so every record fits within
  its 16 KiB limit.
- A Session installs its ContextBinding with an idempotent request. The
  worker checks the shape, the binding's digest, the Workspace part, and
  then operation and Session reuse. A refused request records nothing.
- A shared JSON schema and fixture file pins every closed key set, field
  rule, route, error classification and the check order. An
  implementation independent of both languages generated the expected
  values.
- managed-context-envelope.ts validates boot documents, ready records,
  attestation requests and installation requests, and builds ready
  records, attestation responses and receipts. It reuses the W0a checks
  and digest, which managed-workspace-binding.ts now exports.
- A Java conformance test in runtime-broker pins the key sets, routes,
  constants and error table, and recomputes every installation digest
  with ContextBinding.
- A bilingual design document. The W0a document and the runtime-broker
  README and QWEN.md now point to it.

Refs QwenLM#12380
* feat(sdk-java): project Runtime tool replies onto the broker contract

HttpRuntimeTransport now implements RuntimeTransport. Status and cancel
answers keep only state and a settled result, and session verbs that the
v2 tool contract does not define fail closed.

Co-authored-by: jinye <doudouOUC@users.noreply.github.com>

* feat(sdk-java): adopt local Runtime processes by ready record

The local-process provisioner now reports a durable kind, boots from the
persisted seed, and reconciles a restored lease by attesting it again. A
process that no longer answers is not found instead of guessed ready.

Co-authored-by: jinye <doudouOUC@users.noreply.github.com>

* feat(serve): add the hosted harness profile and broker tool path

qwen serve --profile hosted-harness now requires a loopback bind, a
harness token, and the Runtime Broker origin, token, and capability
digest. The broker client speaks the merged execution routes, and the
local-process provisioner adopts workers by ready record.

Co-authored-by: jinye <doudouOUC@users.noreply.github.com>

* fix(serve): guard incomplete hosted runtime foundations

* fix(serve): close hosted runtime review gaps

* test(core): make exited PID assertion deterministic

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: jinye <doudouOUC@users.noreply.github.com>
Co-authored-by: jinye.djy <jinye.djy@U-Y37CF7G3-1943.local>
Co-authored-by: Shaojin Wen <szujobs@gmail.com>
Bring the standalone managed-agent server, its embedded broker listener,
and the WebShell Java panel onto the current broker contract. Legacy
daemon sessions stay available beside the new path.

Co-authored-by: jinye <doudouOUC@users.noreply.github.com>
@doudouOUC
doudouOUC force-pushed the cursor/managed-agent-spring-webshell-3df3 branch from 72c459e to b08af8f Compare September 25, 2026 15:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants