Skip to content

feat(cli): add extension operation polling - #5753

Merged
wenshao merged 2 commits into
QwenLM:mainfrom
chiga0:feat/extension-polling-api
Jun 23, 2026
Merged

feat(cli): add extension operation polling#5753
wenshao merged 2 commits into
QwenLM:mainfrom
chiga0:feat/extension-polling-api

Conversation

@ytahdn

@ytahdn ytahdn commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Adds an extension operation polling API for daemon-driven extension mutations. Extension install, enable, disable, update, and uninstall requests now return an operation id when accepted, and clients can query that id to observe whether the queued background operation is queued, running, succeeded, failed, or succeeded while session refresh failed. The SDK and daemon workspace action layer expose the new status query so web clients can build polling behavior without relying only on the asynchronous workspace event stream. The extension mutation timeout is also raised to ten minutes so slow network installs have more time to complete.

Why it's needed

Extension installation is asynchronous from the web client perspective: the request is accepted quickly, while the actual install and session refresh happen in the background. Without a polling API, a client can only infer progress from later events, and it cannot reliably inspect the final status for a specific install request. Returning an operation id gives consumers a deterministic way to connect the accepted request with its eventual outcome, including the important partial-success case where installation finished but session refresh failed.

Reviewer Test Plan

How to verify

Run the targeted daemon server tests and confirm extension install responses include an operation id, polling the operation returns succeeded after a successful install, failed after an install failure, succeeded_with_refresh_error when session refresh fails, and 404 for unknown operation ids. Run the SDK client test and confirm the operation id is URL-encoded when querying the status endpoint. Confirm SDK and webui typechecks pass so the new response and action types are consumable.

Evidence (Before & After)

Before: extension mutation requests returned only accepted: true, so a caller could not poll the daemon for the outcome of the specific background operation. After: accepted mutation requests return accepted: true plus operationId, and GET /workspace/extensions/operations/:operationId returns the operation status and result details.

Local verification: cd packages/cli && npx vitest run src/serve/server.test.ts -t "queues extension install and refreshes active sessions|broadcasts a failed extension install|does not report a successful extension install as failed when session refresh fails|returns 404 for unknown extension operation ids" passed with 4 tests. cd packages/sdk-typescript && npx vitest run test/unit/DaemonClient.test.ts -t "extension operations" passed with 1 test. Targeted ESLint and Prettier checks passed for the changed files. cd packages/sdk-typescript && npm run typecheck passed. cd packages/webui && npm run typecheck passed.

Tested on

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

Environment (optional)

Local macOS development checkout using targeted vitest, ESLint, Prettier, and package typecheck commands.

Risk & Scope

  • Main risk or tradeoff: Operation history is intentionally in-memory and capped, so old operation ids are not available after daemon restart or after enough newer operations have been recorded.
  • Not validated / out of scope: This does not add fine-grained install phases such as downloading or extracting, nor does it add persistent operation storage.
  • Breaking changes / migration notes: Extension mutation responses now include operationId in addition to accepted: true; existing consumers that ignore extra fields should continue to work.

Linked Issues

N/A

中文说明

What this PR does

为 daemon 的扩展变更流程增加 operation 轮询接口。扩展安装、启用、禁用、更新、卸载请求被接受后会返回 operation id,客户端可以用这个 id 查询后台任务当前是 queued、running、succeeded、failed,还是安装成功但 session refresh 失败。SDK 和 daemon workspace action 层也暴露了新的状态查询能力,web 客户端可以基于这个接口实现轮询,而不是只依赖异步 workspace event。扩展 mutation 的总超时时间也延长到了十分钟,以便慢网络安装有更多完成时间。

Why it's needed

从 web 客户端视角看,扩展安装是异步的:请求会快速 accepted,真正的安装和 session refresh 在后台完成。没有轮询接口时,客户端只能从后续事件里推断进度,也无法稳定查询某一次 install 请求最终是否成功。返回 operation id 后,消费侧可以把 accepted 请求和最终结果对应起来,也能明确识别“安装完成但 session refresh 失败”这种部分成功状态。

Reviewer Test Plan

How to verify

运行目标 daemon server 测试,确认扩展安装响应包含 operation id,成功安装后轮询返回 succeeded,安装失败后返回 failed,session refresh 失败后返回 succeeded_with_refresh_error,未知 operation id 返回 404。运行 SDK client 测试,确认查询状态接口会正确 URL encode operation id。确认 SDK 和 webui typecheck 通过,说明新的响应类型和 action 类型可被消费。

Evidence (Before & After)

Before:扩展 mutation 请求只返回 accepted: true,调用方无法用 daemon 查询这次后台任务的最终结果。After:被接受的 mutation 请求会返回 accepted: true 和 operationId,并且 GET /workspace/extensions/operations/:operationId 会返回 operation 状态和结果详情。

本地验证:cd packages/cli && npx vitest run src/serve/server.test.ts -t "queues extension install and refreshes active sessions|broadcasts a failed extension install|does not report a successful extension install as failed when session refresh fails|returns 404 for unknown extension operation ids" 通过 4 个测试。cd packages/sdk-typescript && npx vitest run test/unit/DaemonClient.test.ts -t "extension operations" 通过 1 个测试。变更文件的目标 ESLint 和 Prettier 检查通过。cd packages/sdk-typescript && npm run typecheck 通过。cd packages/webui && npm run typecheck 通过。

Tested on

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

Environment (optional)

本地 macOS 开发环境,执行了目标 vitest、ESLint、Prettier 和 package typecheck 命令。

Risk & Scope

  • Main risk or tradeoff: operation 历史有意保持为内存态且有数量上限,因此 daemon 重启后或记录了足够多新 operation 后,旧 operation id 将无法查询。
  • Not validated / out of scope: 本 PR 不增加 downloading、extracting 等细粒度安装阶段,也不增加持久化 operation 存储。
  • Breaking changes / migration notes: 扩展 mutation 响应在 accepted: true 基础上新增 operationId;忽略额外字段的现有消费者应继续正常工作。

Linked Issues

N/A

@ytahdn
ytahdn requested review from qwen-code-ci-bot and wenshao and removed request for wenshao June 23, 2026 07:48
@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

On direction: This is a natural fit. Extension mutations are already async from the client's perspective — accepted immediately, completed in the background. Without a polling mechanism, web clients have no way to correlate a specific install request with its outcome. Adding an operationId + GET endpoint is the standard pattern for this, and the CHANGELOG has no direct reference but the daemon API area is clearly core to the project's serve/webui story.

On approach: Scope feels right and minimal. The in-memory capped operation history (100 entries) is pragmatic — persistent storage would be overkill for a polling cache. The variable narrowing in the install handler (sourcesourceValue etc.) is standard TypeScript closure-narrowing, not unnecessary churn. The succeeded_with_refresh_error state is a nice touch — it captures the important partial-success case that clients need to surface.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

方向:这个功能很合理。扩展变更本身就是异步的——请求被接受后在后台完成。没有轮询机制的话,web 客户端无法把某次安装请求和最终结果对应起来。返回 operationId 加 GET 端点是这类问题的标准做法。CHANGELOG 里没有直接引用,但 daemon API 明显是项目 serve/webui 故事的核心部分。

方案:范围合理且最小化。内存中有上限的 operation 历史(100 条)是务实的选择——对轮询缓存来说持久化存储过度了。install handler 里的变量重命名(sourcesourceValue 等)是标准的 TypeScript 闭包类型收窄,不是无意义的改动。succeeded_with_refresh_error 状态设计得不错——捕获了客户端需要展示的部分成功场景。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Code Review

Independently, I would have designed this almost identically: UUID-keyed in-memory Map for operation tracking, lifecycle states (queued → running → terminal), a GET endpoint for status queries, and SDK/webui wiring. The PR's approach matches this baseline and doesn't miss anything obvious.

No critical issues found:

  • Credential redaction is correctly applied to operation status fields (source URLs, error messages) via redactUrlCredentials, consistent with the existing event redaction pattern.
  • Error truncation to 500 chars prevents unbounded payloads while preserving diagnostic value.
  • Eviction logic correctly uses Map insertion order to remove the oldest terminal operation first, with a guard to keep non-terminal (queued/running) operations from being evicted.
  • Defensive .catch handler (added in the follow-up commit d13162ec) properly redacts and records errors from the queued task itself, not just the inner operation.
  • SDK URL encoding of operationId via encodeURIComponent is correct and verified by test.
  • The install handler variable narrowing (sourcesourceValue, refrefValue, etc.) is the standard TypeScript pattern for closures that capture destructured values — necessary, not bloat.

Test Results

CLI server extension tests (41 passed)

runner@runnervm7b5n9:~/work/qwen-code/qwen-code/.qwen/worktrees/triage$ cd packages/cli && npx vitest run src/serve/server.test.ts -t "extension" 2>&1 | tail -20
 ✓ src/serve/server.test.ts (516 tests | 475 skipped) 1451ms
   ✓ createServeApp > read-only status routes > evicts the oldest terminal extension operations  535ms

 Test Files  1 passed (1)
      Tests  41 passed | 475 skipped (516)
   Start at  11:18:06
   Duration  14.93s (transform 3.98s, setup 126ms, collect 6.14s, tests 1.45s, environment 414ms, prepare 80ms)

Covers: succeeded, failed, succeeded_with_refresh_error, 404 for unknown IDs, queued/running states, eviction of oldest terminal operations (101 iterations), credential redaction in error paths.

SDK extension operations test (1 passed)

runner@runnervm7b5n9:~/work/qwen-code/qwen-code/.qwen/worktrees/triage/packages/sdk-typescript$ npx vitest run test/unit/DaemonClient.test.ts -t "extension operations" 2>&1 | tail -10
 ✓ test/unit/DaemonClient.test.ts  (166 tests | 165 skipped) 8ms

 Test Files  1 passed (1)
      Tests  1 passed | 165 skipped (166)
   Start at  11:20:19
   Duration  521ms (transform 236ms, setup 0ms, collect 262ms, tests 8ms, environment 0ms, setup 78ms)

Verifies URL encoding of operation IDs with / characters.

Typechecks

  • packages/sdk-typescript: clean ✅
  • packages/webui: clean ✅

Real-scenario note

This PR adds a daemon HTTP API endpoint, not a TUI-visible feature. Real-scenario testing would require starting the daemon in a sandbox environment with API keys. The unit tests comprehensively cover all lifecycle states, error paths, and edge cases (eviction, unknown IDs, credential redaction), providing equivalent confidence.

中文说明

代码审查

独立来看,我会用几乎相同的方式设计:UUID 索引的内存 Map 做 operation 追踪,生命周期状态(queued → running → 终态),GET 端点查询状态,以及 SDK/webui 的接入。PR 的方案符合这个预期,没有明显遗漏。

没有发现关键问题:

  • 凭证脱敏 在 operation 状态字段(source URL、error 信息)上通过 redactUrlCredentials 正确应用,与现有的事件脱敏模式一致。
  • 错误截断 到 500 字符,防止无限大的 payload 同时保留诊断价值。
  • 淘汰逻辑 正确使用 Map 插入顺序,优先移除最旧的终态 operation,并保护非终态(queued/running)operation 不被淘汰。
  • 防御性 .catch 处理(在后续提交 d13162ec 中添加)对队列任务本身的错误做了脱敏和记录,不只是内部操作。
  • SDK URL 编码operationId 正确使用 encodeURIComponent,测试验证了含 / 的 ID。
  • install handler 的变量收窄(sourcesourceValue 等)是 TypeScript 闭包捕获解构值的标准做法——必要改动,不是多余的。

测试结果

CLI server 扩展测试:41 通过。覆盖所有生命周期状态、错误路径、淘汰逻辑、凭证脱敏。
SDK extension operations 测试:1 通过。验证 operation ID 的 URL 编码。
Typecheck:sdk-typescript 和 webui 均通过。

本 PR 添加的是 daemon HTTP API 端点,非 TUI 可见功能。真实场景测试需要在沙箱环境中启动 daemon 并配置 API key。单元测试已全面覆盖所有生命周期状态、错误路径和边界情况。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

This is a clean, well-scoped feature PR. The implementation is exactly what I would have proposed independently — UUID-keyed in-memory operation tracking with capped history, proper lifecycle states including the important succeeded_with_refresh_error partial-success case, a straightforward GET endpoint, and clean SDK/webui wiring. The diff (+468/-15 across 8 files) is focused: every line serves the stated goal, with no scope creep or drive-by refactors.

Tests are comprehensive — 41 CLI tests covering all five operation states, eviction, 404 handling, and credential redaction in error paths. The SDK test verifies URL encoding. Both typechecks pass.

The follow-up commit (d13162ec) properly hardens the defensive .catch handler with credential redaction, which shows the author was thinking about security edge cases.

The in-memory-only storage with 100-entry cap is a reasonable tradeoff, clearly documented as intentional.

No concerns. Approving. ✅

中文说明

这是一个干净、范围合理的功能 PR。实现和我独立设想的完全一致——UUID 索引的内存 operation 追踪加上有上限的历史记录,完整的生命周期状态(包括重要的 succeeded_with_refresh_error 部分成功场景),简洁的 GET 端点,以及干净的 SDK/webui 接入。diff(8 个文件,+468/-15)聚焦在目标上:每一行都服务于声明的功能,没有范围蔓延或顺手重构。

测试全面——41 个 CLI 测试覆盖全部五种 operation 状态、淘汰机制、404 处理和错误路径中的凭证脱敏。SDK 测试验证了 URL 编码。两个 typecheck 都通过。

后续提交(d13162ec)正确加固了防御性 .catch 处理的凭证脱敏,说明作者考虑了安全边界情况。

纯内存存储加上 100 条上限是合理的取舍,在 PR 中明确标注为有意设计。

没有顾虑。通过。✅

Qwen Code · qwen3.7-max

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship. ✅

@wenshao

wenshao commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

✅ Local build + real-daemon verification (maintainer merge reference)

Verified PR head 19a231ac9 in a clean isolated worktree: built from a fresh npm ci, ran the full affected suites (not just the -t filtered ones), typechecked + linted every changed package, and exercised the new endpoint against a real qwen serve binary. Everything is green. Three non-blocking observations are listed at the end for your consideration.

1. Environment

Clean git worktree at the PR head · npm ci (1815 packages) · Node v22.22.2 · macOS · real bundle launches (qwen --version0.19.0).

2. Build & static checks

Check Scope Result
npm ci + prepare build whole monorepo ✅ exit 0
tsc --noEmit sdk-typescript ✅ clean
tsc --noEmit webui ✅ clean
eslint all 6 changed source files ✅ clean

3. Tests (full files, fresh build)

Suite Result
packages/clisrc/serve/server.test.ts 514 / 514 passed
packages/sdk-typescripttest/unit/DaemonClient.test.ts 166 / 166 passed

The PR's new/changed tests were confirmed by name:

✓ queues extension install and refreshes active sessions
✓ returns 404 for unknown extension operation ids
✓ broadcasts a failed extension install with redacted error details
✓ does not report a successful extension install as failed when session refresh fails   (→ succeeded_with_refresh_error)
✓ DaemonClient > extension operations > GETs an extension operation status by id          (URL-encodes the id)

Stderr during the run also showed the redaction working: ... background task failed: https://***REDACTED***@example.com/private-ext failed.

4. Live daemon E2E (real qwen serve binary, not the fake bridge)

Started qwen serve --port 4173 --token … --workspace … --no-web and hit the new route directly:

GET /workspace/extensions/operations/does-not-exist-uuid   (Bearer …)
→ 404  {"error":"Extension operation \"does-not-exist-uuid\" not found","code":"extension_operation_not_found"}

GET /workspace/extensions/operations/x   (no Authorization)
→ 401  {"error":"Unauthorized"}

POST /workspace/extensions/install   (Bearer …, X-Qwen-Client-Id: bogus-client, consent:true)
→ 400  {"error":"Client id \"bogus-client\" is not registered …","code":"invalid_client_id"}   (no operation recorded)

So on a real binary: the new route is wired through routing + auth + workspace context, returns the exact SDK-consumable code: extension_operation_not_found, and rejected mutations never create an operation. The four lifecycle terminal states (succeeded / succeeded_with_refresh_error / failed / 404) are covered deterministically by the integration suite above — I did not re-drive them live because that needs a registered ACP client + model auth + a real failing network install, which the unit suite already covers exhaustively.

5. Observations (all non-blocking — merge-safe as-is)

  1. result.source is not redacted (minor consistency / low-risk info-exposure). The top-level source recorded for an operation is redacted via redactUrlCredentials, but the success / refresh-error paths set result: { ...event }, and the install event.source is the raw sourceValue (server.ts:2324). If someone installs from a credentialed URL (https://<token>@host/repo), the raw token is retrievable via the polling endpoint's result.source. Practical risk is low — the operationId is an unguessable UUIDv4 returned only to the issuing client, over an authenticated loopback/bearer channel, and that client already supplied the credential. Still, it diverges from the redaction applied everywhere else; consider redacting result.source (or the whole result) for parity, or confirm it's intentional.

  2. In-memory history cap is safe, but a polled id can 404 after restart / 100 newer ops (already called out in the PR description). I checked the eviction interaction: MAX_EXTENSION_INSTALL_QUEUE_DEPTH = 10MAX_EXTENSION_OPERATION_HISTORY = 100, and the depth check runs before an operation is recorded, so at most 10 non-terminal ops can exist and FIFO eviction can only ever drop already-terminal entries — an in-flight queued/running operation is never evicted. 👍 Good as designed; just flagging the documented "old id → 404" tradeoff for web clients.

  3. Mutation timeout 2 min → 10 min on a serialized queue. Extension mutations run through a single serialized queue, so one slow install now holds the queue (and up to 10 queued mutations behind it) for up to 10 minutes instead of 2. Intended for slow network installs per the PR description — noting the head-of-line-blocking tradeoff only.

Verdict: builds, typechecks, lints, and tests are all green locally, and the new endpoint behaves correctly on a real daemon binary. No blockers. ✅

中文版

✅ 本地构建 + 真实 daemon 验证(维护者合并参考)

在干净的隔离 worktree 里验证了 PR head 19a231ac9:从全新 npm ci 构建,跑了受影响的完整测试文件(不只是 -t 过滤的几个),对每个改动的包做了 typecheck + lint,并用真实 qwen serve 二进制实测了新接口。全部通过。末尾有三条不阻塞合并的观察供你参考。

1. 环境

PR head 上的干净 git worktree · npm ci(1815 个包)· Node v22.22.2 · macOS · 真实 bundle 可启动(qwen --version0.19.0)。

2. 构建与静态检查

检查 范围 结果
npm ci + prepare 构建 整个 monorepo ✅ exit 0
tsc --noEmit sdk-typescript ✅ 通过
tsc --noEmit webui ✅ 通过
eslint 全部 6 个改动源文件 ✅ 通过

3. 测试(完整文件,全新构建)

套件 结果
packages/clisrc/serve/server.test.ts 514 / 514 通过
packages/sdk-typescripttest/unit/DaemonClient.test.ts 166 / 166 通过

逐条确认了本 PR 新增/修改的测试:

✓ queues extension install and refreshes active sessions
✓ returns 404 for unknown extension operation ids
✓ broadcasts a failed extension install with redacted error details
✓ does not report a successful extension install as failed when session refresh fails   (→ succeeded_with_refresh_error)
✓ DaemonClient > extension operations > GETs an extension operation status by id          (对 id 做 URL 编码)

运行过程中 stderr 也显示脱敏生效:... background task failed: https://***REDACTED***@example.com/private-ext failed

4. 真实 daemon 端到端(真实 qwen serve 二进制,非 fake bridge)

启动 qwen serve --port 4173 --token … --workspace … --no-web,直接打路由:

GET /workspace/extensions/operations/does-not-exist-uuid   (Bearer …)
→ 404  {"error":"Extension operation \"does-not-exist-uuid\" not found","code":"extension_operation_not_found"}

GET /workspace/extensions/operations/x   (无 Authorization)
→ 401  {"error":"Unauthorized"}

POST /workspace/extensions/install   (Bearer …, X-Qwen-Client-Id: bogus-client, consent:true)
→ 400  {"error":"Client id \"bogus-client\" is not registered …","code":"invalid_client_id"}   (不会记录 operation)

也就是说在真实二进制上:新路由的路由 + 鉴权 + workspace context 都串通了,返回的正是 SDK 可消费的 code: extension_operation_not_found,被拒绝的 mutation 也不会创建 operation。四个生命周期终态(succeeded / succeeded_with_refresh_error / failed / 404)已由上面的集成套件确定性覆盖——我没有再用真实网络重跑这些,因为那需要已注册的 ACP client + 模型鉴权 + 一次真实失败的网络安装,而单测已经把这些穷尽覆盖了。

5. 观察(均不阻塞合并,当前状态可直接合)

  1. result.source 未脱敏(轻微一致性 / 低风险信息暴露)。 operation 顶层记录的 source 经过 redactUrlCredentials 脱敏,但 success / refresh-error 路径里 result: { ...event },而 install 的 event.source原始sourceValueserver.ts:2324)。如果用带凭据的 URL 安装(https://<token>@host/repo),原始 token 可通过轮询接口的 result.source 读回。实际风险很低——operationId 是不可猜测的 UUIDv4、只返回给发起方、走鉴权后的 loopback/bearer 通道,而该 client 本来就是凭据的提供方。但这和其它地方的脱敏不一致;建议把 result.source(或整个 result)也脱敏以保持一致,或确认这是有意为之。

  2. 内存历史上限是安全的,但旧 id 在重启 / 100 个更新的 operation 之后会 404(PR 描述已说明)。我核了驱逐逻辑的相互作用:MAX_EXTENSION_INSTALL_QUEUE_DEPTH = 10MAX_EXTENSION_OPERATION_HISTORY = 100,且深度检查在记录 operation 之前执行,所以最多只有 10 个非终态 operation,FIFO 驱逐只可能删掉已经是终态的条目——正在进行的 queued/running operation 永远不会被驱逐。👍 设计是稳的;只是把这个已记录的"旧 id → 404"权衡提示给 web 客户端。

  3. mutation 超时 2 分钟 → 10 分钟,且队列是串行的。 扩展 mutation 走单一串行队列,所以一个慢安装现在最多会占住队列(以及它后面最多 10 个排队的 mutation)10 分钟,而不是原来的 2 分钟。按 PR 描述这是为慢网络安装有意调大的——这里只提示队头阻塞的权衡。

结论: 本地构建、typecheck、lint、测试全绿,新接口在真实 daemon 二进制上行为正确。无阻塞项。✅

Comment thread packages/cli/src/serve/server.ts Outdated
Comment thread packages/cli/src/serve/server.ts Outdated
Comment thread packages/cli/src/serve/server.ts
Comment thread packages/cli/src/serve/server.ts
@wenshao

wenshao commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

✅ Local runtime verification (real daemon, not just unit tests)

I built the real esbuild bundle from this PR's head (d13162ec8) and drove the new extension‑operation polling API end‑to‑end against a live qwen serve daemon (real curl + the TypeScript SDK), in addition to the targeted unit suites.

Environment

  • Bundle: node esbuild.config.jsdist/cli.js (≈6.3 MB). Confirmed the new code is compiled in (markers extension_operation_not_found, succeeded_with_refresh_error, workspace/extensions/operations).
  • Daemon: qwen serve --hostname 127.0.0.1 --port 4180 with QWEN_SERVER_TOKEN, isolated QWEN_HOME, in tmux. A real session was created to obtain a registered X-Qwen-Client-Id (required by the mutation routes).
  • Real extension installs over the network (GitHub). Node v22.

Results

Layer Scope Result
Live daemon GET …/operations/:id unknown id → 404 extension_operation_not_found
Live daemon install non‑existent repo → 202 {accepted, operationId}runningfailed (real git clone error)
Live daemon install marketplace‑only repo → failed (distinct error path)
Live daemon install a real single extension → running…succeeded, result {refreshed:1, failed:0}, actually installed on disk
Live daemon disable an installed extension → succeeded, result {status:"disabled", refreshed:1}
Live daemon every mutation 202 carries operationId; states transition queued → running → terminal; ops stay queryable in history
Live daemon (SDK) DaemonClient.extensionOperationStatus(id) round‑trips; special‑char id is encodeURIComponent‑encoded (server echoes the exact id in 404)
Unit server.test.ts (install→succeeded, failed, succeeded_with_refresh_error, 404) ✅ 4/4
Unit DaemonClient.test.ts (extension operations URL‑encoding) ✅ 1/1

Sample — success path (live):

POST /workspace/extensions/install {source:"https://github.com/DanielLetto2020/qwen-playwright-extension", consent:true}
  → 202 {"accepted":true,"operationId":"67c9b5c2-…"}
poll → running → running → running → succeeded
GET  …/operations/67c9b5c2-… →
  { "v":1, "operation":"install", "status":"succeeded",
    "result":{ "status":"installed","name":"qwen-playwright-extension","version":"1.2.0",
               "refreshed":1,"failed":0 } }

Sample — failure path (live):

POST …/install {source:"https://github.com/ytahdn/this-extension-does-not-exist-zzz999", consent:true}
  → 202 {operationId:"0c580776-…"} ; poll → running×3 → failed
  status.error = "Failed to clone Git repository … remote: Repository not found. …"   (sliced to 500 chars)

Credential redaction is confirmed by the unit test (daemon log shows https://***REDACTED***@example.com/… for a credentialed source), and the succeeded_with_refresh_error partial‑success state is exercised there (mutation succeeded but refresh failed).

Notes (non‑blocking)

  • Operation history is an in‑memory Map capped at 100, evicting only terminal entries; non‑terminal entries are bounded by the install queue, so it stays bounded. History resets on daemon restart — both are intentional per the PR's Risk & Scope.
  • GET …/operations/:id requires the bearer token but not a registered client‑id (unlike the mutations). It's read‑only, keyed by an unguessable UUID, and source/error are redacted, so this isn't a concern — noting only for awareness.

Verdict

Works as designed across all documented states (queued/running/succeeded/failed/succeeded_with_refresh_error + 404), on a real daemon and in the unit suites. Operation ids are returned and pollable, credentials are redacted, and the SDK helper round‑trips correctly. No blocking issues found — LGTM. ✅

中文版(合并参考)

✅ 本地真实运行时验证(真实 daemon,而非仅单元测试)

我基于本 PR 的最新 head(d13162ec8)构建了真实的 esbuild bundle,并针对运行中的 qwen serve daemon 对新增的扩展 operation 轮询接口做了端到端验证(真实 curl + TypeScript SDK),同时跑了目标单元测试。

环境

  • Bundle:node esbuild.config.jsdist/cli.js(≈6.3 MB)。已确认新代码被编译进去(标记 extension_operation_not_foundsucceeded_with_refresh_errorworkspace/extensions/operations)。
  • Daemon:qwen serve --hostname 127.0.0.1 --port 4180,配置 QWEN_SERVER_TOKEN、隔离 QWEN_HOME,运行在 tmux 中。创建了一个真实 session 以获得已注册的 X-Qwen-Client-Id(mutation 路由要求)。
  • 通过网络从 GitHub 真实安装扩展。Node v22。

结果

层级 范围 结果
真实 daemon GET …/operations/:id 未知 id → 404 extension_operation_not_found
真实 daemon 安装不存在的仓库 → 202 {accepted, operationId}runningfailed(真实 git clone 错误)
真实 daemon 安装 marketplace 类型仓库 → failed(另一条错误路径)
真实 daemon 安装一个真实的单一扩展 → running…succeededresult {refreshed:1, failed:0},且确实落盘安装
真实 daemon disable 已安装扩展 → succeededresult {status:"disabled", refreshed:1}
真实 daemon 每个 mutation 的 202 都带 operationId;状态按 queued → running → terminal 流转;历史可持续查询
真实 daemon(SDK) DaemonClient.extensionOperationStatus(id) 正常往返;含特殊字符的 id 会被 encodeURIComponent 编码(服务端在 404 中原样回显该 id)
单元 server.test.ts(install→succeeded、failed、succeeded_with_refresh_error、404) ✅ 4/4
单元 DaemonClient.test.ts(扩展 operation 的 URL 编码) ✅ 1/1

成功路径示例(真实):

POST /workspace/extensions/install {source:"https://github.com/DanielLetto2020/qwen-playwright-extension", consent:true}
  → 202 {"accepted":true,"operationId":"67c9b5c2-…"}
轮询 → running → running → running → succeeded
GET  …/operations/67c9b5c2-… →
  { "v":1, "operation":"install", "status":"succeeded",
    "result":{ "status":"installed","name":"qwen-playwright-extension","version":"1.2.0",
               "refreshed":1,"failed":0 } }

失败路径示例(真实):

POST …/install {source:"https://github.com/ytahdn/this-extension-does-not-exist-zzz999", consent:true}
  → 202 {operationId:"0c580776-…"};轮询 → running×3 → failed
  status.error = "Failed to clone Git repository … remote: Repository not found. …"(截断到 500 字符)

凭据脱敏由单元测试确认(daemon 日志中带凭据的 source 显示为 https://***REDACTED***@example.com/…),并且 succeeded_with_refresh_error 这一“部分成功”状态也在那里被覆盖(mutation succeeded but refresh failed)。

说明(非阻塞)

  • operation 历史是内存中的 Map,上限 100,且只淘汰终态条目;非终态条目受 install 队列约束,因此整体有界。daemon 重启后历史会清空——这两点都是 PR 在 Risk & Scope 中明确的预期行为。
  • GET …/operations/:id 需要 bearer token,但不需要已注册的 client‑id(与 mutation 不同)。它是只读的、以不可猜测的 UUID 为 key,且 source/error 已脱敏,因此不构成问题——仅作提示。

结论

在真实 daemon 与单元测试中,所有文档化状态(queued/running/succeeded/failed/succeeded_with_refresh_error + 404)均按设计工作。operation id 会返回并可轮询、凭据已脱敏、SDK helper 往返正常。未发现阻塞性问题——LGTM。✅

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

No blocking issues found. Two non-blocking suggestions below for consideration.

— qwen3.7-max via Qwen Code /review

? refreshErr.message
: String(refreshErr),
);
updateExtensionOperation(operationId, {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The catch (refreshErr) block lacks a try/catch isolation guard. The new updateExtensionOperation call here is safe today (just a Map.set), but bridge.broadcastExtensionsChanged and writeStderrLine below are not wrapped. If either throws, the error cascades to the outer .catch on enqueueExtensionInstall, which overwrites the operation record from succeeded_with_refresh_error to failed and broadcasts a spurious failed event — creating a status inversion where the extension is installed but clients see failed.

Wrapping this block in a try/catch (matching the defensive pattern already used in the outer catch) would prevent the cascade:

Suggested change
updateExtensionOperation(operationId, {
try {
updateExtensionOperation(operationId, {
status: 'succeeded_with_refresh_error',
result: {
...redactExtensionOperationResult(event),
refreshed: 0,
failed: 1,
error: message.slice(0, 500),
},
});
bridge.broadcastExtensionsChanged({
...event,
error: message,
});
writeStderrLine(
`qwen serve: extensions ${operation}: mutation succeeded but refresh failed: ${message}`,
);
} catch (innerErr) {
try { writeStderrLine(`qwen serve: extensions ${operation}: refresh-error handler threw: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`); } catch { /* guard */ }
}

— qwen3.7-max via Qwen Code /review

@@ -1255,7 +1255,7 @@ export function createServeApp(
extensionInstallQueue = next.catch(() => undefined);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The 10-minute timeout now applies uniformly to all five mutation types (install, enable, disable, update, uninstall), but only install involves network I/O that benefits from the longer window. Enable/disable/update/uninstall are local filesystem operations that typically complete in under a second. If one of these hangs (e.g., a filesystem lock), the caller waits 10 minutes and the queue slot is occupied the entire time.

Consider splitting into two constants — EXTENSION_INSTALL_TIMEOUT_MS = 10 * 60_000 for the install path and keeping EXTENSION_MUTATION_TIMEOUT_MS = 120_000 for the others — to limit the blast radius of a hung local operation.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship. ✅

@wenshao
wenshao merged commit 8ad0cde into QwenLM:main Jun 23, 2026
80 checks passed
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.

3 participants