Skip to content

feat(sdk-java): Add daemon transport - #7463

Merged
doudouOUC merged 7 commits into
QwenLM:mainfrom
doudouOUC:agent/java-daemon-sdk-alpha
Jul 23, 2026
Merged

feat(sdk-java): Add daemon transport#7463
doudouOUC merged 7 commits into
QwenLM:mainfrom
doudouOUC:agent/java-daemon-sdk-alpha

Conversation

@doudouOUC

Copy link
Copy Markdown
Collaborator

What this PR does

This PR adds a Java 11 daemon transport to the existing com.alibaba:qwencode-sdk artifact and prepares version 0.1.0-alpha without introducing a second Maven artifact. The new API creates thread-scoped sessions, separates prompt admission from terminal completion, streams typed and raw daemon events, supports permission responses and session cancellation, and provides a bounded promptText() convenience API.

The transport uses a non-retried POST admission followed by resumable SSE from the returned watermark. It sends Accept-Encoding: identity and Last-Event-ID, validates UTF-8, JSON and SSE framing strictly, advances cursors only after observer delivery, deduplicates replayed events, rejects gaps and compressed streams, and reports ambiguous mutations or missing reliable terminals through explicit exception types. Client-owned executors, heartbeat scheduling, stream cleanup, detach-once close, and explicit destroy semantics keep resource use and lifecycle outcomes bounded.

The daemon and ACP paths now close the reliability contracts the Java client depends on: every admitted prompt receives one correlated terminal, deadlines cover queueing and execution, queued and running cancellation are distinguished, admission-aware cancellation waits for the targeted call to settle, and session teardown flushes prompt terminals before session failure events. The SDK documentation, design record, Java 11 migration guidance, cross-platform CI, real-daemon E2E harness, and protected Maven Central release workflow are included.

Why it's needed

The legacy Java stdio API is not a daemon transport and cannot safely reconstruct a prompt after an HTTP response races streamed events or an SSE connection drops. Requiring each Java application to implement admission watermarks, SSE parsing, replay cursors, reconnect policy, deduplication, terminal correlation, cancellation, and lifecycle cleanup would duplicate difficult protocol code and can surface partial output as a successful answer.

Some guarantees also cannot be repaired in an SDK alone. A client cannot synthesize a trustworthy terminal that the daemon never published, distinguish an old event cursor after a daemon epoch change without a resync signal, or prove which queued prompt an unacknowledged session-scoped cancellation affected. This change therefore closes the daemon/ACP contract first and builds the Java client on top of those explicit guarantees.

Reviewer Test Plan

How to verify

  1. From packages/sdk-java/qwencode, run mvn --batch-mode --no-transfer-progress clean verify -Dgpg.skip=true. Expect 103 tests with zero failures or errors, five integration tests skipped outside the E2E harness, zero Checkstyle violations, and main, source, and Javadoc JARs for 0.1.0-alpha.
  2. From the repository root, run npm run build && npm run typecheck && npm run bundle. Expect every workspace to build and typecheck successfully and the CLI bundle to be produced.
  3. Run npx tsx scripts/run-java-daemon-sdk-e2e.ts. Expect four Java E2E tests to pass against a real local qwen serve process and model stub, covering create, text/tool/permission streaming, reliable completion, deadline, cancellation, teardown ordering, detach, and destroy.
  4. Run npx vitest run src/bridge.test.ts from packages/acp-bridge, then npx vitest run src/acp-integration/acpAgent.test.ts src/acp-integration/session/Session.test.ts from packages/cli. Expect 434 bridge tests and 692 CLI tests to pass.
  5. Review the fault-injection tests for early terminals, SSE fragmentation, replay duplicates, event gaps, malformed frames, compressed responses, reconnect exhaustion, observer failures, blocked stream cleanup, lost mutation responses, repeated close, and UTF-8 text limits. Each unsafe or unverifiable outcome should fail closed rather than return partial success.

Evidence (Before & After)

N/A — this is a non-UI SDK, protocol, documentation, and release-automation change.

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested locally; CI configured
🐧 Linux ⚠️ not tested locally; CI and real-daemon E2E configured

Environment (optional)

macOS Darwin 25.4.0 arm64, Java 21.0.8 LTS, Node.js 22.22.3, and npm 10.9.8. The Java CI matrix also targets Java 11, 17, and 21.

Risk & Scope

  • Main risk or tradeoff: This is a cross-package daemon/ACP reliability change plus a new concurrent Java transport, so cancellation ordering, terminal publication, executor saturation, and teardown behavior require maintainer review. The API is intentionally alpha and favors fail-closed outcomes over silently returning partial data.
  • Not validated / out of scope: Maven Central credentials and production signing were not exercised locally; external reverse proxies and local Windows/Linux execution were not tested. CI covers the supported OS/JDK matrix. Cross-daemon-restart exactly-once execution, automatic snapshot/resync recovery, and true public prompt-ID-targeted cancellation remain explicit alpha limitations.
  • Breaking changes / migration notes: com.alibaba:qwencode-sdk:0.1.0-alpha raises the minimum Java version for the whole artifact from Java 8 to Java 11 and removes Logback as a runtime dependency. Java 8 applications must remain on 0.0.3-alpha; applications should provide their preferred SLF4J backend. The Maven coordinates do not change and the legacy stdio API remains available.

Linked Issues

Related: #7386, #7400

中文说明

此 PR 的内容

此 PR 在现有 com.alibaba:qwencode-sdk 制品中新增 Java 11 daemon transport,并准备发布 0.1.0-alpha,不引入第二个 Maven 制品。新 API 默认创建 thread scope 会话,分别暴露 prompt admission 与 terminal completion,流式提供类型化事件和原始 daemon 事件,支持权限响应与会话取消,并提供有界的 promptText() 便捷 API。

传输流程使用不自动重试的 POST admission,然后从响应 watermark 建立可恢复的 SSE。客户端发送 Accept-Encoding: identityLast-Event-ID,严格校验 UTF-8、JSON 和 SSE framing,仅在 observer 成功处理后推进游标,对回放事件去重,并拒绝事件 ID 缺口和压缩流。mutation 结果不确定或缺少可靠终态时通过明确的异常类型返回。客户端自有线程池、heartbeat 调度、流清理、最多一次 detach 的 close 以及显式 destroy 语义共同保证资源使用和生命周期结果有界。

daemon 与 ACP 路径补齐了 Java 客户端依赖的可靠性契约:每个已 admission 的 prompt 都会收到一个关联终态;deadline 同时覆盖排队与执行;区分 queued 和 running cancellation;admission-aware cancellation 会等待目标调用结算;session teardown 会先刷新 prompt 终态,再发送 session failure 事件。同时包含 SDK 文档、设计记录、Java 11 迁移说明、跨平台 CI、真实 daemon E2E 工具,以及受保护的 Maven Central 发布工作流。

为什么需要此改动

旧 Java stdio API 并不是 daemon transport,无法在 HTTP 响应与流式事件竞态或 SSE 连接中断时可靠地重建一次 prompt。如果要求每个 Java 应用自行实现 admission watermark、SSE 解析、回放游标、重连策略、去重、终态关联、取消与生命周期清理,会重复大量高风险协议代码,并可能把不完整输出误报为成功答案。

部分保证也无法由 SDK 独立弥补。daemon 未发布终态时,客户端无法凭空生成可信终态;daemon epoch 变化后,如果没有 resync 信号,客户端无法识别旧事件游标;未确认的 session scope cancel 也无法证明实际影响了哪个排队 prompt。因此本改动先闭环 daemon/ACP 契约,再在这些明确保证之上实现 Java 客户端。

Reviewer 验证计划

验证方法

  1. packages/sdk-java/qwencode 运行 mvn --batch-mode --no-transfer-progress clean verify -Dgpg.skip=true。预期 103 个测试零失败、零错误,E2E harness 外有 5 个 integration 测试跳过,Checkstyle 零违规,并生成 0.1.0-alpha 的主 JAR、source JAR 和 Javadoc JAR。
  2. 在仓库根目录运行 npm run build && npm run typecheck && npm run bundle。预期所有 workspace 构建和类型检查成功,并生成 CLI bundle。
  3. 运行 npx tsx scripts/run-java-daemon-sdk-e2e.ts。预期 4 个 Java E2E 测试在真实本地 qwen serve 进程和模型桩上通过,覆盖 create、text/tool/permission 流、可靠完成、deadline、取消、teardown 顺序、detach 与 destroy。
  4. packages/acp-bridge 运行 npx vitest run src/bridge.test.ts,然后在 packages/cli 运行 npx vitest run src/acp-integration/acpAgent.test.ts src/acp-integration/session/Session.test.ts。预期 434 个 bridge 测试和 692 个 CLI 测试全部通过。
  5. 检查终态早于响应、SSE 分片、回放重复、事件缺口、非法 frame、压缩响应、重连耗尽、observer 失败、流清理阻塞、mutation 响应丢失、重复 close 和 UTF-8 文本限制等故障注入测试。所有不安全或不可验证的结果都应 fail closed,不得返回部分成功。

证据(Before & After)

N/A——这是非 UI 的 SDK、协议、文档和发布自动化改动。

测试平台

OS 状态
🍏 macOS ✅ 已测试
🪟 Windows ⚠️ 本地未测试;已配置 CI
🐧 Linux ⚠️ 本地未测试;已配置 CI 和真实 daemon E2E

环境(可选)

macOS Darwin 25.4.0 arm64、Java 21.0.8 LTS、Node.js 22.22.3、npm 10.9.8。Java CI 矩阵还覆盖 Java 11、17 和 21。

风险与范围

  • 主要风险或取舍:这是跨 package 的 daemon/ACP 可靠性改动,并新增了并发 Java transport,因此取消顺序、终态发布、线程池饱和与 teardown 行为需要 maintainer 重点评审。API 明确处于 alpha 阶段,优先 fail closed,避免静默返回部分数据。
  • 未验证或范围外:本地未使用 Maven Central 凭证和生产签名;未验证外部反向代理,也未在本地 Windows/Linux 上执行。CI 覆盖支持的 OS/JDK 矩阵。跨 daemon 重启的 exactly-once、自动 snapshot/resync 恢复以及真正公开的 prompt-ID targeted cancel 仍是明确的 alpha 限制。
  • 破坏性变更与迁移说明:com.alibaba:qwencode-sdk:0.1.0-alpha 将整个制品的最低 Java 版本从 Java 8 提升到 Java 11,并移除 Logback runtime 依赖。Java 8 应用必须继续使用 0.0.3-alpha,应用应自行提供所需的 SLF4J backend。Maven 坐标不变,旧 stdio API 继续保留。

关联事项

相关:#7386#7400

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

Built the PR base vs this PR head c8b54aa, drove a fixed endpoint set against each, and diffed the JSON responses. Only fields that changed are shown.

No response changes against the PR base across 4 scenario(s).

Qwen Code · serve A/B

@doudouOUC doudouOUC self-assigned this Jul 22, 2026
@doudouOUC
doudouOUC marked this pull request as ready for review July 22, 2026 03:06
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR — this is a substantial piece of work.

Template looks good ✓

Problem: This is a feature addition, not a bug fix. The motivation is clear: the legacy Java stdio API cannot safely reconstruct a prompt after an HTTP response races streamed events or an SSE connection drops. The merged prerequisites (#7386 detach ledger, #7400 exactly-once terminals) already landed, and this builds the Java client on top of those contracts. The problem space is real for anyone integrating qwen serve from Java.

Direction: Aligned — the project already ships packages/sdk-java with a stdio API, and daemon mode is an established feature. A daemon transport is the natural next step. No direct Claude Code CHANGELOG reference for a Java SDK, but the daemon direction is well-represented. That said, this PR touches release infrastructure (Maven Central publishing workflow with secrets), introduces a new ACP protocol extension (craft/cancelPendingPrompt), and modifies the public SDK contract — all of which warrant maintainer sign-off on direction.

Size: This is a cross-package change (acp-bridge, cli, sdk-java) touching core infrastructure. Breakdown: ~5,385 production logic lines (additions + deletions, excluding test files) vs. ~3,414 test lines. This is well above the 500-line threshold for maintainer awareness on a feat PR, and above the 1,000-line large-PR advisory. Flagging for maintainer attention — not blocking on size alone, but this needs a human eye on the cross-package contract changes.

Approach: The design is thorough — admission watermarks, per-prompt SSE subscription, terminal correlation, fail-closed ambiguity handling. The ACP bridge changes (admission-aware cancellation handshake, prompt-id-based cancel dedup, removed-prompt terminal flush) are tightly coupled to what the Java client needs, so they belong together. However, the PR also bundles CI workflows, a Maven Central release pipeline, docs, and an E2E harness — consider whether the release workflow and docs could land as a follow-up to keep the reviewable surface smaller. The new craft/cancelPendingPrompt extension is a protocol-level addition that changes the ACP contract; a custom ACP child that doesn't implement it falls back to standard session/cancel, which is reasonable, but the extension itself needs maintainer review.

Moving on to code review. 🔍

中文说明

感谢贡献!这是一个相当大的工程。

模板完整 ✓

问题: 这是一个功能新增,不是 bug 修复。动机很明确:旧 Java stdio API 无法在 HTTP 响应与流式事件竞态或 SSE 连接中断时安全地重建 prompt。前置 PR(#7386 detach 账本、#7400 精确一次终态)已合并,本 PR 在这些契约之上构建 Java 客户端。对于需要从 Java 集成 qwen serve 的用户来说,这个问题空间是真实的。

方向: 对齐——项目已有 packages/sdk-java 和 stdio API,daemon 模式也是成熟功能。daemon transport 是自然的下一步。Claude Code CHANGELOG 中没有 Java SDK 的直接参考,但 daemon 方向有充分体现。不过,此 PR 触及发布基础设施(Maven Central 发布工作流,含 secrets)、引入新的 ACP 协议扩展(craft/cancelPendingPrompt)、并修改公共 SDK 契约——这些都需要 maintainer 在方向上签字确认。

规模: 这是跨 package 改动(acp-bridge、cli、sdk-java),触及核心基础设施。分解:约 5,385 行生产逻辑(additions + deletions,不含测试文件)vs. 约 3,414 行测试。远超 feat PR 的 500 行维护者关注阈值,也超过 1,000 行大 PR 建议线。标记供维护者关注——不单纯因规模阻塞,但跨 package 契约变更需要人工审查。

方案: 设计很详尽——admission watermark、per-prompt SSE 订阅、终态关联、fail-closed 歧义处理。ACP bridge 变更(admission-aware 取消握手、基于 prompt-id 的取消去重、已移除 prompt 的终态刷新)与 Java 客户端需求紧密耦合,放在一起合理。但 PR 同时打包了 CI 工作流、Maven Central 发布管线、文档和 E2E 工具——建议考虑将发布工作流和文档作为后续 PR 单独提交,以缩小可审查面。新的 craft/cancelPendingPrompt 扩展是协议级新增,改变了 ACP 契约;不实现该扩展的自定义 ACP child 会回退到标准 session/cancel,这合理,但扩展本身需要维护者审查。

进入代码审查 🔍

Qwen Code · qwen3.7-max

Reviewed at 05fd7a5c1dc1524373e1ed2fd2a11c7c407422b6 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: For a Java daemon transport, I'd build a DaemonClient managing HTTP/SSE connections, a DaemonSessionClient for session lifecycle, and a per-prompt execution flow: POST admission → SSE from watermark → terminal correlation. Cancellation would need an admission-aware handshake in the ACP bridge so the SDK can reliably cancel prompts. Fault injection tests with an in-process HTTP server would cover SSE fragmentation, replay, gaps, and ambiguous mutations.

Comparison with the diff: The PR's approach matches this closely and goes further — bounded semaphores for prompt/stream/future capacity, a separate stream-close pool so stalled closes can't block deadlines, and a promptText() convenience that enforces UTF-8 byte limits. The ACP bridge changes are well-scoped: prompt-id-based cancel dedup (replacing the boolean latch), admission-aware cancellation via craft/cancelPendingPrompt, and removed-prompt terminal flush for teardown.

No critical blockers found. The code is well-structured and the fail-closed design is consistent throughout. A few observations for the maintainer:

  • The broadcastTurnError change adds a mutateTurnState parameter — when a queued (non-running) prompt is cancelled, the turn error no longer sets retryAllowed = true or records turnError. This is intentional and correct (a cancelled queued prompt shouldn't mutate the active turn's state), but it's a subtle behavioral change worth a second look.
  • The removed flag on PendingPromptEntry keeps running prompts in the internal list until settlement so teardown can flush terminals. The public queue API filters them out. This adds complexity but solves a real problem — without it, session teardown could miss publishing a terminal for a removed-but-running prompt.
  • The new craft/cancelPendingPrompt extension falls back to standard session/cancel for non-implementing ACP children (via the -32601 method-not-found check). The FIFO drain fence (cancelForwardDrain) ensures no extension request is in flight when prompt ownership advances.
  • Java code quality is high — clean Java 11, proper resource management with AutoCloseable, bounded thread pools, and a comprehensive exception hierarchy that distinguishes ambiguous outcomes from definitive failures.
sequenceDiagram
    participant P1 as Java SDK
    participant P2 as Daemon REST
    participant P3 as SSE Stream
    participant P4 as ACP Bridge
    participant P5 as ACP Child
    P1->>P2: POST /session (create)
    P2-->>P1: sessionId, clientId
    P1->>P2: POST /session/id/prompt (admit)
    P2-->>P1: 202 promptId, lastEventId
    P1->>P3: GET /session/id/events (Last-Event-ID)
    P3-->>P1: stream typed events
    P1->>P2: POST /session/id/cancel
    P2->>P4: craft/cancelPendingPrompt
    P4->>P5: abort + await settlement
    P5-->>P4: settled
    P4-->>P2: cancelled true
    P3-->>P1: turn_complete (stopReason cancelled)
    P1->>P2: POST /session/id/detach
    P2-->>P1: 204
Loading
Files changed (30 of 59 shown)
File What changed
.github/workflows/release-sdk-java.yml New Maven Central release workflow with GPG signing, dry-run support, and tag/artifact idempotency
.github/workflows/sdk-java.yml New CI workflow - Java 11/17/21 matrix on Linux, macOS, Windows plus real-daemon E2E
docs/design/java-daemon-sdk-alpha.md Design doc covering wire flow, transport contract, ambiguity handling, and alpha non-goals
docs/developers/sdk-java.md Updated SDK docs - Java 11 requirement, daemon transport usage, SLF4J migration
packages/acp-bridge/src/bridge.ts Admission-aware cancel handshake, prompt-id cancel dedup, removed-prompt terminal flush, deadline re-throw
packages/acp-bridge/src/bridgeTypes.ts New PROMPT_CANCEL_METHOD constant and PendingPromptEntry fields (cancelForward, dispatched, removed)
packages/acp-bridge/src/bridge.test.ts 279 new lines testing cancel forwarding, dedup, removed-prompt flush, and deadline re-throw
packages/cli/src/acp-integration/acpAgent.ts ActivePromptCall tracking, craft/cancelPendingPrompt handler with abort-and-await-settlement
packages/cli/src/acp-integration/session/Session.ts Admission cancellation signal support - abort during assertCanStartTurn returns cancelled
packages/cli/src/serve/server.ts Re-export PromptDeadlineExceededError from acp-session-bridge instead of prompt-deadline
packages/cli/src/serve/server/prompt-deadline.ts Removed re-exported PromptDeadlineExceededError class (moved to acp-bridge)
packages/sdk-java/.../DaemonClient.java Client factory - bounded pools, semaphores, HTTP/SSE transport, capabilities check
packages/sdk-java/.../DaemonSessionClient.java Session lifecycle - prompt admission, SSE observation, terminal correlation, heartbeat, detach/destroy
packages/sdk-java/.../SseReader.java Strict SSE parser - LF/CRLF framing, UTF-8 validation, frame size limits, retry directive
packages/sdk-java/.../HttpSupport.java Bounded HTTP body reader with deadline racing via sendAsync
packages/sdk-java/.../JsonSupport.java Strict Jackson Core decoding - rejects duplicate keys, non-standard JSON
packages/sdk-java/.../PromptCall.java Public prompt handle - admission and completion futures, terminal publication gate
packages/sdk-java/.../PromptRequest.java Prompt request builder with deadline, observation timeout, and metadata
packages/sdk-java/.../PromptTerminal.java Terminal event model - COMPLETE vs ERROR kind with stop reason
packages/sdk-java/.../PromptObserver.java Observer interface - onText, onThought, onTool, onUsage, onPermission, onEvent
packages/sdk-java/.../DaemonEvent.java Typed event wrapper with prompt correlation and session validation
packages/sdk-java/.../PromptTextResult.java Bounded text result from promptText convenience API
packages/sdk-java/.../PermissionRequest.java Permission request model parsed from SSE events
packages/sdk-java/.../PermissionResponse.java Permission response builder (approve/deny with optional message)
packages/sdk-java/.../CreateSessionRequest.java Session creation request with scope, model, and approval mode
packages/sdk-java/.../DaemonCapabilities.java Capabilities model - version, mode, features, transports
packages/sdk-java/.../exception types (8 files) Hierarchy distinguishing ambiguous vs definitive failures per mutation type
packages/sdk-java/.../DaemonSessionClientTest.java 2596-line fault-injection suite - SSE fragmentation, replay, gaps, compression, observer failure
scripts/run-java-daemon-sdk-e2e.ts E2E harness running Java tests against a real qwen serve process with model stub
... and 29 more files Java test updates, pom.xml, README, RELEASE.md, QWEN.md

Real-Scenario Testing

No Java/Maven or tmux in this CI environment. Verified the TypeScript side end-to-end: build, typecheck, all affected tests, and a live daemon serve session exercising the new endpoints.

Build and typecheck:

$ npm run build   # all workspaces compiled successfully
$ npm run typecheck  # tsc --noEmit passed for all packages

Unit tests (PR code):

$ cd packages/acp-bridge && npx vitest run src/bridge.test.ts
 ✓ src/bridge.test.ts (434 tests) 5205ms
 Test Files  1 passed (1)
      Tests  434 passed (434)

$ cd packages/cli && npx vitest run src/acp-integration/acpAgent.test.ts
 ✓ src/acp-integration/acpAgent.test.ts (300 tests) 14685ms
 Test Files  1 passed (1)
      Tests  300 passed (300)

$ cd packages/cli && npx vitest run src/acp-integration/session/Session.test.ts
 ✓ src/acp-integration/session/Session.test.ts (392 tests) 25825ms
 Test Files  1 passed (1)
      Tests  392 passed (392)

Live daemon serve test (this PR's build):

$ node dist/cli.js serve --port 18923

$ curl -s http://localhost:18923/capabilities
{"v":1,"mode":"http-bridge","features":[...,"session_scope_override",
"client_heartbeat","session_cancel","session_events",...],
"transports":["rest"],...}

$ curl -s -X POST http://localhost:18923/session \
    -H 'Content-Type: application/json' -d '{"sessionScope":"thread"}'
{"sessionId":"04ed0a59-5c0c-4206-a025-0b8f865e2ba2",
 "workspaceCwd":"...","attached":false,
 "clientId":"client_ff9ac241-0368-41f0-88a2-0ed2343db8fb",
 "createdAt":"2026-07-22T03:24:22.079Z"}

$ curl -s -X POST http://localhost:18923/session/04ed.../cancel \
    -H "X-Qwen-Client-Id: client_ff9ac241..." -d '{}' -w "%{http_code}"
204

$ curl -s -X POST http://localhost:18923/session/04ed.../detach \
    -H "X-Qwen-Client-Id: client_ff9ac241..." -d '{}' -w "%{http_code}"
204

Server log:
  cancel forwarded via session/cancel -> "Not currently generating"
    (expected - no active prompt)
  session closed (reason: last_client_detached)

Capabilities correctly advertise session_scope_override, client_heartbeat, and transports: ["rest"] — the three features the Java SDK requires. Session creation, cancel, and detach all return expected status codes. The cancel forwarding path works correctly (the "Not currently generating" error is the expected ACP child response when no prompt is active).

Not tested: Java SDK compilation and tests (no JDK/Maven in this environment), real-daemon E2E harness, Maven Central release workflow. CI covers these.

中文说明

代码审查

独立方案: 对于 Java daemon transport,我会构建一个管理 HTTP/SSE 连接的 DaemonClient、一个管理会话生命周期的 DaemonSessionClient,以及 per-prompt 执行流程:POST admission → 从 watermark 建立 SSE → 终态关联。取消需要在 ACP bridge 中实现 admission-aware 握手,让 SDK 能可靠地取消 prompt。故障注入测试使用进程内 HTTP 服务器覆盖 SSE 分片、回放、缺口和歧义 mutation。

与 diff 对比: PR 的方案与此高度一致,并走得更远——用有界信号量管理 prompt/stream/future 容量,独立的 stream-close 线程池防止阻塞 deadline,以及 promptText() 便捷 API 强制 UTF-8 字节限制。ACP bridge 变更范围合理:基于 prompt-id 的取消去重(替代布尔锁存器)、通过 craft/cancelPendingPrompt 实现 admission-aware 取消、以及已移除 prompt 的终态刷新。

未发现关键阻塞问题。 代码结构良好,fail-closed 设计贯穿始终。几点供维护者参考:

  • broadcastTurnError 变更增加了 mutateTurnState 参数——当排队(非运行中)的 prompt 被取消时,turn error 不再设置 retryAllowed = true 或记录 turnError。这是有意且正确的(取消的排队 prompt 不应修改活跃 turn 的状态),但属于微妙的行为变更,值得再看一眼。
  • PendingPromptEntry 上的 removed 标志让运行中的 prompt 在结算前保留在内部列表中,以便 teardown 能刷新终态。公共队列 API 会过滤掉它们。这增加了复杂度,但解决了真实问题——没有它,session teardown 可能遗漏已移除但运行中 prompt 的终态发布。
  • 新的 craft/cancelPendingPrompt 扩展对不实现该扩展的 ACP child 回退到标准 session/cancel(通过 -32601 method-not-found 检查)。FIFO drain fence(cancelForwardDrain)确保 prompt 所有权推进时没有扩展请求在途。
  • Java 代码质量高——干净的 Java 11、proper 资源管理(AutoCloseable)、有界线程池、以及区分歧义结果和确定性失败的完整异常层次。

真实场景测试

此 CI 环境无 Java/Maven 和 tmux。已端到端验证 TypeScript 侧:构建、类型检查、所有受影响测试、以及实际 daemon serve 会话验证新端点。

构建和类型检查全部通过。1126 个单元测试全部通过(434 bridge + 300 acpAgent + 392 Session)。实际 daemon serve 测试确认 capabilities 正确广播 session_scope_overrideclient_heartbeattransports: ["rest"]。Session 创建、取消、分离均返回预期状态码。

未测试: Java SDK 编译和测试(环境无 JDK/Maven)、真实 daemon E2E 工具、Maven Central 发布工作流。CI 覆盖这些。

Qwen Code · qwen3.7-max

Reviewed at 05fd7a5c1dc1524373e1ed2fd2a11c7c407422b6 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — clean review across every stage, but the Stage 0 core-module escalation (5,385 production lines, cross-package ACP/CLI changes, new protocol extension, release infrastructure) needs a maintainer's sign-off.

This is a well-executed PR. The design doc is thorough, the Java code is clean and well-organized, the fail-closed approach to ambiguity is the right call for an SDK, and the ACP bridge changes are tightly scoped to what the daemon transport actually needs. All 1,126 TypeScript tests pass, build and typecheck are clean, and the live daemon serve test confirms the new endpoints work correctly.

Going back to my independent proposal — the PR matches it closely and exceeds it in the capacity management layer (bounded semaphores, separate stream-close pool) and the fault-injection test suite (2,596 lines covering SSE fragmentation, replay, gaps, compression, observer failure). I didn't find a simpler path it missed. The complexity is earned: daemon transport with SSE reconnection, terminal correlation, and ambiguity handling is inherently complex.

Why 3/5 and not higher:

  1. Scale and cross-package reach. 5,385 production lines across acp-bridge, cli, and sdk-java. The bridge.ts changes (admission-aware cancellation, prompt-id dedup, removed-prompt terminal flush) modify the daemon's reliability contract — exactly the kind of change that needs a maintainer who knows the full consumer graph.
  2. New ACP protocol extension. craft/cancelPendingPrompt is a protocol-level addition. The fallback to standard session/cancel is well-handled, but the extension itself changes the contract between the bridge and ACP children.
  3. Release infrastructure. The Maven Central publishing workflow with GPG signing and production secrets is new release automation that warrants maintainer review before merge.
  4. Java side unverified locally. No JDK/Maven in this environment — the 103 Java tests and E2E harness are covered by CI but not independently verified here.

None of these are blockers — they're the reason this needs a human maintainer's eye rather than an auto-approve.

中文说明

置信度:3/5 — 每个阶段审查都很干净,但 Stage 0 核心模块升级(5,385 行生产代码、跨 package ACP/CLI 变更、新协议扩展、发布基础设施)需要维护者签字确认。

这是一个执行良好的 PR。设计文档详尽,Java 代码干净且组织良好,fail-closed 的歧义处理方式是 SDK 的正确选择,ACP bridge 变更紧密围绕 daemon transport 的实际需求。所有 1,126 个 TypeScript 测试通过,构建和类型检查干净,实际 daemon serve 测试确认新端点工作正常。

回到我的独立方案——PR 与之高度一致,并在容量管理层(有界信号量、独立 stream-close 线程池)和故障注入测试套件(2,596 行,覆盖 SSE 分片、回放、缺口、压缩、observer 失败)上超出预期。我没有找到它遗漏的更简路径。复杂度是合理的:带有 SSE 重连、终态关联和歧义处理的 daemon transport 本身就是复杂的。

为什么是 3/5 而不是更高:

  1. 规模和跨 package 范围。 5,385 行生产代码横跨 acp-bridge、cli 和 sdk-java。bridge.ts 的变更(admission-aware 取消、prompt-id 去重、已移除 prompt 终态刷新)修改了 daemon 的可靠性契约——这正是需要了解完整消费者图的维护者应该审查的变更。
  2. 新 ACP 协议扩展。 craft/cancelPendingPrompt 是协议级新增。回退到标准 session/cancel 处理得当,但扩展本身改变了 bridge 和 ACP child 之间的契约。
  3. 发布基础设施。 Maven Central 发布工作流(含 GPG 签名和生产 secrets)是新的发布自动化,合并前需要维护者审查。
  4. Java 侧未本地验证。 此环境无 JDK/Maven——103 个 Java 测试和 E2E 工具由 CI 覆盖,但未在此独立验证。

这些都不是阻塞问题——而是需要人工维护者审查而非自动批准的原因。

Qwen Code · qwen3.7-max

Reviewed at 05fd7a5c1dc1524373e1ed2fd2a11c7c407422b6 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

⏸️ Deferring to @pomelo-nwu — this PR is clean across all review stages (no blockers found, all 1,126 TS tests pass, live daemon test confirms correct behavior), but it needs a maintainer's sign-off before merge because:

  1. 5,385 production lines touching core infrastructure (acp-bridge, cli, sdk-java) — well above the 500-line maintainer-awareness threshold for feat PRs.
  2. New ACP protocol extension (craft/cancelPendingPrompt) changes the bridge-to-child contract.
  3. New Maven Central release workflow with GPG signing and production secrets.
  4. Cross-package reliability contract changes in bridge.ts (admission-aware cancellation, prompt-id dedup, removed-prompt terminal flush) affect every daemon consumer.

The code quality is high and the design is sound — this is a policy escalation, not a quality concern. Needs a human call on this one.

@gwinthis

Copy link
Copy Markdown
Collaborator

🔬 Local Verification Report — PR #7463

PR: feat(sdk-java): add daemon transport (commit 05fd7a5c1)
Scope: 59 files, +8,275 / -524 lines
Environment: macOS darwin, Node.js v22.22.1, JDK 21.0.7, Maven 3.x
Date: 2026-07-22

Verdict

建议修改后合并。 传输层协议实现正确,全部 1,225 个测试通过(含 4 个真实 daemon E2E)。但存在 2 个需要 PR 作者确认的设计问题(见 §5),以及 1 个文档缺陷需要补充。


1. Build

Step Result
npm install --ignore-scripts
npm run build
npm run bundle

⚠️ Finding (DX): npm run build 不更新根目录 dist/cli.js(esbuild bundle)。E2E 脚本 run-java-daemon-sdk-e2e.ts 依赖 dist/cli.js,必须先 npm run bundle。PR 文档 docs/developers/sdk-java.md 和脚本注释均未说明此依赖。初始 E2E 因此全量失败(DaemonProtocolException: transports must be an array),bundle 后恢复。建议 PR 补充说明。

2. TypeScript Unit Tests

文件 总测试数 PR 新增 结果
acp-bridge/bridge.test.ts 434 +6
cli/acpAgent.test.ts 300 +2
cli/Session.test.ts 392 +2
合计 1,126 +10 ✅ 0 failures

覆盖范围: PR 涉及 5 个核心 TS 改动文件,测试覆盖了其中 3 个(bridge.ts、acpAgent.ts、Session.ts)。以下 2 个文件无直接测试覆盖:

  • packages/cli/src/serve/server.ts(export 路径变更,见 §5.3)
  • packages/cli/src/serve/server/prompt-deadline.ts(删除了 PromptDeadlineExceededError re-export)

3. Java SDK Unit Tests

测试类 测试数 类型 结果
DaemonSessionClientTest 83 Mock(内嵌 HTTP server)
JsonSupportTest 7 纯单元
SseReaderTest 4 纯单元
HttpSupportTest 1 纯单元
DaemonServeE2ETest 4 集成(需真实 daemon) ⏭️ 此阶段跳过
合计 99 ✅ 95 passed, 0 failed

4. tmux E2E Test(真实 Daemon + Java SDK)

通过 scripts/run-java-daemon-sdk-e2e.ts 在 tmux 中运行:fake OpenAI server → qwen serve --require-auth → Maven DaemonServeE2ETest

E2E 测试 结果
runsPromptToolPermissionAndTerminalAgainstRealDaemon
deadlineTerminalIsReliableAndSessionRemainsReusable
cancelledPromptReceivesReliableTerminal
teardownDeliversTerminalBeforeSessionFailure
合计 ✅ 4/4, 5.9s

验证的事件流: session_update → replay_complete → permission_request → permission_resolved → approval_mode_changed → turn_complete

⚠️ 局限性: E2E 使用 mock 模型服务器(固定响应),验证的是传输层协议正确性(HTTP 路由、SSE 事件流、permission 交互、cancel/deadline 终端事件),不验证真实模型响应解析、token 计数、context 压缩、工具执行等模型交互质量。

5. Code Review Findings

5.1 ActivePromptCall hang 风险(需作者确认)

PROMPT_CANCEL_METHOD 处理器执行 controller.abort()await Promise.all(settled)settledsession.prompt()finally 块中 resolve。

问题: 如果 session.prompt() 不响应 AbortSignal(例如底层 HTTP 请求无 abort 支持),settled 永远不 resolve,cancel 请求也 hang。activePromptCalls Map 中的条目永远不会被清理。

建议: 确认 session.prompt() 在所有代码路径上都响应 abort signal,或在 cancel 处理器中增加超时保护。

5.2 cancelBroadcastPromptId 跨 prompt 竞态(需作者确认)

从 boolean latch 改为 promptId 追踪后,dedup 逻辑为:entry.cancelBroadcastPromptId === promptId 时抑制重复广播。

边界场景: prompt A 的 cancel 请求在 prompt B 启动后到达。此时 cancelBroadcastPromptId 为空(prompt B 启动时未重置),prompt A 的 cancel 会被广播——但 prompt A 已经结束。这不会导致错误(broadcastPromptCancelled 对已完成的 prompt 是 no-op),但会产生一条无意义的 SSE 事件。

影响: 低。不会导致状态错误,仅产生冗余事件。建议作者确认是否需要处理。

5.3 PromptDeadlineExceededError export 路径变更(向后兼容)

  • 旧路径: packages/cli/src/serve/server/prompt-deadline.ts re-export
  • 新路径: packages/cli/src/serve/acp-session-bridge.ts 直接 export,server.ts re-export

验证: grep -rn "from.*prompt-deadline" 确认无残留旧导入。server.ts 的 re-export 保持外部 API 不变。无向后兼容风险。

5.4 broadcastTurnError mutateTurnState 参数

仅 1 个调用点,传入 pendingEntry.state === 'running'。语义正确:仅当 prompt 实际在运行时才修改 retryAllowed/turnError 状态,排队中的 prompt 不产生副作用。✅

6. 未验证维度

维度 状态 说明
CI 状态 ❓ 未检查 gh 认证失效,无法查询。merge 前需确认 CI 绿
与 main 的冲突 ⚠️ PR 落后 main 100 commits 无冲突(0 behind),但 base 较旧,建议 rebase
安全性 ❓ 未深入 token 通过 QWEN_SERVER_TOKEN 环境变量传递,--require-auth 强制 bearer 认证。未做渗透测试
性能 ❓ 未测量 ActivePromptCall 每次 prompt 增加 Map set/delete 操作,高频场景开销未量化
代码覆盖率 ❓ 未获取 Java JaCoCo 报告未单独提取

7. 测试汇总

类别 数量 结果
TS 单元测试 1,126(含 PR 新增 10) ✅ 全部通过
Java 单元测试 95 ✅ 全部通过
tmux E2E(mock 模型) 4 ✅ 全部通过
合计 1,225 ✅ 0 failures

8. Merge 前建议

  1. 必须: 确认 CI 绿
  2. 必须: 作者回复 §5.1(abort signal 响应性)和 §5.2(跨 prompt cancel 竞态)
  3. 建议: 补充 E2E 前置条件文档(npm run bundle 依赖)
  4. 建议: Rebase 到最新 main(当前落后 100 commits)

Tested locally with tmux on macOS. Full logs available upon request.

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

Reviewed. Suggestions are inline. Not reviewed: chunk 3, chunk 12, chunk 13, chunk 16, chunk 18, chunk 8, chunk 10, chunk 20, chunk 7, chunk 11, chunk 17, chunk 15, chunk 9, chunk 2, chunk 5, chunk 4, chunk 1, chunk 19, chunk 14, chunk 6 — launched with a prompt that is not the one the CLI built. Not reviewed: Agent 0: Issue fidelity & root-cause ownership, Test coverage matrix (whole-diff), Agent 1b: Removed-behavior audit, Agent 1c: Cross-file tracer, Agent 7: Build & test verification, Invariant agent A: state, timers, collections — packages/sdk-java/qwencode/QWEN.md, Invariant agent B: counters, return values, error taxonomies — packages/sdk-java/qwencode/QWEN.md, Invariant agent C: config fields, early returns — packages/sdk-java/qwencode/QWEN.md — its prompt was built, but no agent on record was launched with it. Not reviewed: reverse audit — an auditor ran and opened its brief, but no agent was launched with the prompt the CLI built — the launch was written by hand, and what the agent was actually asked is not what this skill certifies. Not reviewed: verification — a verifier ran and opened its brief, but no agent was launched with the prompt the CLI built — the launch was written by hand, and the posted findings cannot be counted as verified against it.

— qwen3.7-max via Qwen Code /review

Comment thread packages/acp-bridge/src/bridge.ts
doudouOUC added a commit to doudouOUC/qwen-code that referenced this pull request Jul 22, 2026
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC
doudouOUC force-pushed the agent/java-daemon-sdk-alpha branch from 05fd7a5 to 9f40201 Compare July 22, 2026 04:24
@github-actions

Copy link
Copy Markdown
Contributor

Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration.

中文

请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

@gwinthis Thanks for the detailed review. I addressed the actionable E2E workflow gap and rebased the branch onto the current main.

  • The source E2E documentation now requires both npm run build and npm run bundle, and the harness fails fast with that exact instruction when dist/cli.js is absent.
  • I confirmed the §5.1 uncooperative-AbortSignal risk. I did not add an acknowledgement-only timeout: releasing the FIFO before the cancelled call settles could let a late session-scoped cancel hit its successor. The design, developer guide, package README, and release notes now state that such a session is outcome-unknown and must not be reused; reclaimable ACP-child/session isolation remains outside the alpha.
  • I could not reproduce the §5.2 cross-prompt cancelBroadcastPromptId race. The broadcast is prompt-ID keyed and synchronous, and the FIFO waits for the cancellation-forward drain before dispatching a successor. A session-scoped cancel processed only after B starts targets B by contract because the request carries no A prompt ID. The full bridge and CLI cancellation suites pass on the rebased commit.

Validated at 9f4020103bb5824a37ebac61a426d19e9b86dc93: ACP Bridge 439/439, CLI 693/693, Java unit tests 98 passed plus 5 expected skips in the normal Maven run, real daemon E2E 4/4, and relevant workspace typechecks. The repository-wide build currently reaches unrelated Web Shell type errors already present at origin/main; none of the affected files are in this PR.

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Follow-up summary for 9f4020103bb5824a37ebac61a426d19e9b86dc93:

Review item Decision Action
E2E can execute a missing/stale root bundle Accepted Documented the required bundle step and added a missing-bundle fail-fast check.
Cancellation may never settle when an integration ignores AbortSignal Accepted as an alpha limitation Documented fail-closed session handling; rejected an unsafe acknowledgement-only timeout that could violate FIFO cancellation isolation.
Late A cancel can publish A after B starts Not reproduced / not applicable to the current contract Verified prompt-ID-keyed synchronous broadcast plus the FIFO cancel-forward drain with the full affected test suites.
Branch behind main Accepted Rebased cleanly onto current main.

Validation: relevant TypeScript typechecks passed; ACP Bridge 439/439; CLI 693/693; Maven verification 103 tests with 98 passed and 5 expected skips; real daemon E2E 4/4. Repository-wide build is independently blocked by Web Shell type errors already present in origin/main and outside this PR's diff.

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

Reviewed. Unresolved, please confirm: [Critical] DaemonSessionClient.java:565 — activePrompt not cleared after post-admission observation failure; author defends as intentional fail-closed reuse boundary, verified by test contract Not reviewed: coverage — could not read the agents' transcripts (no subagent transcripts at /home/github-runner/actions-runner-15/_work/_temp/qwen-home/projects/-home-github-runner-actions-runner-15--work-qwen-code-qwen-code/subagents/4c871d68-e283-48ff-961b-8a8689e80e8f (ENOENT: no such file or directory, scandir '/home/github-runner/actions-runner-15/_work/_temp/qwen-home/projects/-home-github-runner-actions-runner-15--work-qwen-code-qwen-code/subagents/4c871d68-e283-48ff-961b-8a8689e80e8f'). The harness writes one per agent; if there are none, either no agents ran or the harness could not write them.), so this run cannot show that any of the diff was read. Not reviewed: verification — could not check that Step 4 and Step 5 ran (no subagent transcripts at /home/github-runner/actions-runner-15/_work/_temp/qwen-home/projects/-home-github-runner-actions-runner-15--work-qwen-code-qwen-code/subagents/4c871d68-e283-48ff-961b-8a8689e80e8f (ENOENT: no such file or directory, scandir '/home/github-runner/actions-runner-15/_work/_temp/qwen-home/projects/-home-github-runner-actions-runner-15--work-qwen-code-qwen-code/subagents/4c871d68-e283-48ff-961b-8a8689e80e8f'). The harness writes one per agent; if there are none, either no agents ran or the harness could not write them.).

— qwen3.7-max via Qwen Code /review

@gwinthis

Copy link
Copy Markdown
Collaborator

🔬 Delta Review — PR #7463 Update (commit 9f4020103)

Delta scope: 1 new commit codex: address PR review feedback, 5 files, +48/-2
Rebase: PR rebased onto newer main (26 serve-related files brought in, +3,710 lines from main, not PR-authored)
Date: 2026-07-22


上轮 Review Findings 逐条核验

# 上轮 Finding 本轮状态 说明
§1 DX npm run bundle 前置条件未文档化 已修复 sdk-java.mdREADME.md 新增 "Real daemon E2E from source" 章节;run-java-daemon-sdk-e2e.ts 新增 existsSync(cliBundle) 前置检查,缺失时抛出明确错误信息
§5.1 ActivePromptCall hang 风险 已回应(设计决策) 设计文档、README、RELEASE.md 均新增段落:明确说明 cancel handshake 故意不设 acknowledgement-only timeout,因为那会让 late session-scoped cancel 到达下一个 prompt。如果 provider/tool 忽略 AbortSignal,session 应被销毁而非复用。这是 alpha 契约的已知限制,不是 bug
§5.2 cancelBroadcastPromptId 跨 prompt 竞态 未改动 上轮评估为低影响(仅产生冗余 SSE 事件,不导致状态错误),作者未修改,可接受
§5.3 PromptDeadlineExceededError export 路径 ✅ 上轮已确认无风险 无变化
§5.4 broadcastTurnError mutateTurnState ✅ 上轮已确认正确 无变化

新提交代码审查

scripts/run-java-daemon-sdk-e2e.ts (+11/-1):

  • 新增 existsSync(cliBundle) 检查 ✅ — 错误信息准确("run npm run build && npm run bundle")
  • path.join(root, 'dist', 'cli.js') 提取为 cliBundle 变量复用 ✅ — 消除了重复路径拼接

文档变更 (sdk-java.md, README.md, RELEASE.md, java-daemon-sdk-alpha.md):

  • 三处文档(sdk-java.md、README.md、java-daemon-sdk-alpha.md)新增了相同的 cancel handshake 说明段落,内容一致 ✅
  • RELEASE.md 的 "Known alpha limits" 补充了 timeout 设计理由 ✅
  • 措辞准确:"a provider or tool that ignores its AbortSignal can therefore leave the session unusable until stronger runtime isolation is available" — 明确了责任边界和恢复路径

重新验证结果(最新版本)

类别 数量 结果 vs 上轮
TS: bridge.test.ts 439 +5(rebase 带入)
TS: acpAgent.test.ts 301 +1(rebase 带入)
TS: Session.test.ts 392 不变
Java 单元测试 95 不变
tmux E2E(mock 模型) 4 不变
合计 1,231 ✅ 0 failures

结论

上轮 2 个必须项(§1 DX 文档、§5.1 abort 风险)均已解决。 §1 通过代码+文档双重修复;§5.1 通过设计文档明确为 alpha 契约的已知限制(fail-closed 语义:session 不可复用则销毁),这是合理的设计决策。

建议合并。 剩余 §5.2(跨 prompt cancel 冗余事件)为低影响 cosmetic 问题,不阻塞 merge。


Delta review on latest commit 9f4020103. Full verification logs available upon request.

doudouOUC added a commit to doudouOUC/qwen-code that referenced this pull request Jul 22, 2026
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC
doudouOUC force-pushed the agent/java-daemon-sdk-alpha branch from 9f40201 to b4c779f Compare July 22, 2026 05:45
@gwinthis
gwinthis requested review from gwinthis and qwen-code-ci-bot and removed request for gwinthis July 22, 2026 05:47
@doudouOUC
doudouOUC requested review from gwinthis and yiliang114 July 22, 2026 05:50
gwinthis
gwinthis previously approved these changes Jul 22, 2026
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

CI follow-up for c8b54aa31d:

Item Action
Windows Java 21 lifecycle-lock test Fixed a scheduling race in the test by holding the SSE stream before any terminal event, so close() is exercised while the prompt is deterministically active. No production behavior changed.
Verification The target test passed 10 consecutive Java 21 runs; mvn -Dgpg.skip=true verify passed all 103 tests with 0 Checkstyle violations.

The previous failure could occur when the immediate terminal event won the race before close(), making the prompt correctly complete successfully while the test incorrectly required an indeterminate outcome.

@wenshao

wenshao commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Code Review — feat(sdk-java): Add daemon transport (#7463)

Reviewed at head b4c779f0. Scope: the ~2.6k-line new Java 11 daemon transport SDK plus the cross-package daemon/ACP reliability changes it depends on. I verified the TypeScript changes by typecheck + running the new targeted tests (2 Session, 2 acpAgent, 10 bridge — all green) and audited the Java core along the concurrency / SSE / terminal-correlation paths.

Overview

  • New com.alibaba.qwen.code.daemon.* client: thread-scoped sessions, non-retried POST admission → resumable SSE from a watermark, Accept-Encoding: identity + Last-Event-ID, strict UTF-8/JSON/SSE validation, cursor-after-delivery, replay dedup, gap/compression rejection, and explicit fail-closed exception types for ambiguous outcomes. Bumps the shared qwencode-sdk artifact to 0.1.0-alpha (Java 8 → 11, Logback dropped from runtime).
  • Bridge/ACP side closes the reliability contracts the client relies on: per-prompt (not per-turn) cancel-broadcast dedup, admission-aware cancellation via a new craft/cancelPendingPrompt extension that waits for the targeted call to settle without cancelling a background turn, deadline errors distinguished from user cancel, and terminal flush for removed/running prompts before session teardown.

Overall assessment

High-quality, defensively written, genuinely fail-closed. The Java core's hardest invariants hold under scrutiny — SSE cursor advances only after observer delivery, replay dedup / gap detection are off-by-one-free (including the lastEventId=0 first-connect case), terminal correlation rejects cross-prompt/cross-session/conflicting terminals, stop() vs claimTerminal are mutually exclusive via outcomeLock, interrupt status is restored on every InterruptedException path, and partial output is never returned as success. The stop/close ↔ reconnect race is deliberately closed by the stopFailure re-check right after activeStream.set(...). Executors are all internally owned and reference-counted on shutdown (no client-supplied-executor foot-gun). No Critical/High/Medium defect surfaced across two independent passes. Everything below is Low / polish.

Findings

TypeScript (bridge/ACP)

  1. forwardRunningPromptCancel fallback doesn't fail-open on "not currently generating"packages/acp-bridge/src/bridge.ts:1801. When an ACP agent lacks the craft/cancelPendingPrompt extension, the -32601 branch falls back to entry.connection.cancel(notification) unguarded. If that races the turn settling and throws a NotCurrentlyGenerating error (or a third-party agent returns invalidParams for a torn-down session), it propagates out of the memoized promise and rejects cancelSession — whereas the plain idle path at bridge.ts:5694 explicitly swallows isNotCurrentlyGeneratingCancelError. A benign no-op /cancel can thus surface as an error. Narrow (only third-party agents without the extension; the first-party agent added here always answers), but it's a behavioral regression vs. pre-PR for that population. Suggest wrapping the fallback cancel in the same isNotCurrentlyGeneratingCancelError guard.

  2. broadcastPromptCancelledOnce no longer dedups a undefined promptIdbridge.ts:984. The old boolean latch suppressed a second broadcast unconditionally; the new prompt-id keying skips both the check and the record when promptId === undefined, so two /cancels against an idle session now emit two prompt_cancelled frames instead of one. Minor — the code comments note consumers treat idle cancels idempotently.

  3. Nitbridge.ts:5684 aborts a dispatched running prompt with reason 'Prompt cancelled before dispatch'. Diagnostic-only, but the message is factually wrong on that branch.

Java — SSE spec-compliance / robustness (all Low)

  1. REST path doesn't reject a compressed body while the SSE path doesDaemonClient.send() sends Accept-Encoding: identity (DaemonClient.java:465) but never checks the response Content-Encoding, whereas DaemonSessionClient.validateSseHeaders() (:1048) rejects non-identity. A Content-Encoding: gzip REST response reaches HttpSupport.decode() and throws a confusing invalid UTF-8 error (fails safe, but inconsistent with the SSE guard the authors clearly intended).
  2. SseReader.readLine() doesn't treat a lone CR as a terminatorSseReader.java:118. The SSE spec allows CR / LF / CRLF; a bare-\r server accumulates the whole stream into one line → no frame dispatched → read as "stream ended before terminal." No mainstream server emits bare CR, hence Low.
  3. parseId/parseRetry use Character.isDigit (SseReader.java:155,177), which accepts non-ASCII digits (e.g. ٧), then Long.parseLong throws → reported as "id outside long range" rather than "not an integer." Value still rejected; misleading message only.
  4. No leading UTF-8 BOM stripSseReader.java:143. A BOM would turn the first field name into "data", silently dropping that field. Daemon doesn't emit a BOM.
  5. Comment/keepalive lines count toward the frame-size limitSseReader.java:34. Many :-comment keepalives with no intervening blank line can trip "SSE frame exceeds N bytes" on an otherwise-healthy idle connection (default 16 MB, so needs an atypical keepalive style).

Java — deadline / lifecycle edges (all Low)

  1. Duration.ZEROIllegalArgumentException on a sub-ms deadline raceDaemonSessionClient.java:659. If the clock crosses the deadline between the checkStoppedOrExpired at :654 and remainingMillis(...), a Duration.ZERO request timeout is rejected by the JDK and unwinds to the Throwable catch, wrapping as PromptOutcomeIndeterminateException("Prompt observation failed", IllegalArgumentException) instead of the clean "timed out" cause. Observable outcome (indeterminate) is still correct — cosmetic cause only.
  2. Deadline-math overflow → spurious immediate timeoutremainingNanos (:1095) can overflow to negative only when deadlineAfter saturates to Long.MAX_VALUE (absurd multi-century observation timeout) and System.nanoTime() is currently negative. Real-world impact minimal; 30-min default never saturates.
  3. TextCollector.finish() under-counts a trailing unpaired high surrogate by 1 byte (:1148); the in-stream case is handled and tested. Malformed-server edge, bounded.
  4. Stale interrupt on the pooled worker after stop() unblocks via stream closerun()'s finally (:576) never clears Thread.interrupted(); tolerated in practice by the executor/latch loops but latent fragility.
  5. closeStreamAsync leaks the InputStream if the stream-close executor rejectsDaemonClient.java:362. Only reachable if streamCloseExecutor is saturated/shut down, which the semaphore + activeStreamLifecycles==0 shutdown gate appears to prevent — reported as a defensive gap, low confidence it's reachable.

Test coverage

Strong overall — the fault-injection suite (early terminals, SSE fragmentation, replay dupes, gaps, malformed frames, compressed responses, reconnect exhaustion, observer failures, blocked cleanup, lost mutations, repeated close, UTF-8 limits) is exactly the right shape, and the new bridge/Session/acpAgent tests pin the cancellation contracts. Gaps worth closing:

  • HttpSupportTest is the thinnest — the BoundedBodySubscriber overflow/cancel logic, the 1 MB success / 64 KB error body caps, and strict-UTF-8 rejection on the success path are untested.
  • JsonSupportTest: deep nesting, the narrowInteger int/long boundary, exactLong out-of-range.
  • SseReaderTest: exactly the bare-CR / BOM / field-with-no-value / non-ASCII-digit cases from findings 5–7.

Nits

  • pom.xml now pulls in three JSON-adjacent stacks (Jackson for strict parse, fastjson2 for serialize, plus the SDK helpers). Intentional and each has a role, but worth a one-line comment so a future reader doesn't try to collapse them.
  • JsonSupport.optionalString/optionalObject/optionalList build error messages from field alone, unlike the required* helpers which prefix context. Cosmetic inconsistency.

Nothing here blocks merge for an alpha; items 1, 4, and the HttpSupportTest gap are the ones I'd prioritize.

@doudouOUC

doudouOUC commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough review. I checked each item against c8b54aa31d. This PR has already passed roughly five review/fix rounds, and none of these findings meets the repository's Critical-only threshold for further changes; the review also confirms that none blocks the alpha release. To keep the landing diff stable, I am recording the following deferrals for a focused follow-up issue/PR after #7463 lands:

Finding Disposition
1 — extension fallback can surface a benign not-generating cancel error Defer; valid low-impact compatibility polish for third-party agents without craft/cancelPendingPrompt, but not a release blocker.
2 — undefined promptId idle-cancel broadcasts are not deduplicated Defer; redundant idempotent notification only.
3 — pre-dispatch wording on a dispatched cancel Defer; diagnostic wording only.
4 — REST compressed-response error classification Defer; current behavior fails closed, with only error clarity/consistency affected.
5 — lone-CR SSE line endings Defer; interoperability hardening for an atypical server encoding.
6 — non-ASCII digit error classification Defer; input is still rejected, only the message differs.
7 — leading SSE BOM Defer; daemon does not emit BOM and the case is interoperability hardening.
8 — comment-only keepalive frame-size accounting Defer; requires atypical framing plus roughly 16 MB without a blank separator.
9 — sub-millisecond deadline race cause Defer; outcome remains correctly indeterminate.
10 — saturated multi-century deadline overflow Defer; outside practical configured durations.
11 — trailing unpaired-surrogate byte count Defer; malformed-server edge with bounded impact.
12 — stale worker interrupt Defer; no demonstrated functional failure in the owned executor paths.
13 — rejected stream-close task defensive path Defer; no demonstrated reachable path under the semaphore/shutdown invariants.
HttpSupportTest, JsonSupportTest, and SseReaderTest coverage gaps Defer as follow-up test hardening; current fault-injection coverage and release gates remain green.
JSON-stack explanation and optional-field error-context nits Defer as documentation/message polish.

No code changes are being added for this review round.

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully. Run review failed. See workflow logs for details. A transient error is retried automatically; if you are seeing this, retry with @qwen-code /review. See workflow logs.

doudouOUC added a commit to doudouOUC/qwen-code that referenced this pull request Jul 23, 2026
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Review follow-up for b8bcd2bd59:

Item Action Rationale
Reject userinfo in daemon base URI Fixed Prevents credentials from being embedded in request URIs; added a regression test.
Clamp HTTP-date Retry-After during parsing Deferred The reconnect path already caps all parsed values at 5 seconds; this is non-critical defense-in-depth and the PR is past the review-round expansion threshold.

Validation: root build and typecheck passed; Java mvn -Dgpg.skip=true clean verify passed with 106 tests, 5 environment-dependent skips, and 0 Checkstyle violations. The initial Qwen Review job failure was runner infrastructure (No space left on device) after its review was successfully posted.

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

Reviewed. Suggestions are inline. Unresolved, please confirm: [Critical] DaemonSessionClient.java:565 — activePrompt not cleared after post-admission observation failure; author defends as intentional fail-closed reuse boundary; the mechanism (activePrompt retained after admission) is present in the code by design, but whether this constitutes a defect or correct behavior requires maintainer judgment Not reviewed: build-and-test — Java build/test could not run (no JDK/Maven on runner); TypeScript tests passed 1134/1134.

— qwen3.7-max via Qwen Code /review

doudouOUC added a commit to doudouOUC/qwen-code that referenced this pull request Jul 23, 2026
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Review follow-up for 4cef807480:

Review item Action Result
Error-path collector.finish() can mask an indeterminate outcome Fixed PromptOutcomeIndeterminateException now remains the primary result, with bounded surrogate-safe partial text preserved. Added a regression for an orphan high surrogate at the byte limit followed by an SSE ID gap.
Retaining activePrompt after a post-admission observation failure Not changed This is the documented fail-closed contract: once execution may have occurred but reliable terminal state is unavailable, reusing the same session could overlap or misattribute prompts. Existing tests require PromptAlreadyActiveException; callers must close/destroy the session before continuing.

Validation:

  • mvn -Dgpg.skip=true clean verify: 107 tests, 0 failures/errors, 5 environment-gated skips; Checkstyle clean.
  • Root npm run build && npm run typecheck: passed after restoring the generated node_modules/ink directory from the clean package and applying the repository patch once.
  • Independent reproduction confirmed the prior exception masking and the regression test now covers it.

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

Reviewed. Suggestions are inline. Unresolved, please confirm: [Critical] DaemonSessionClient.java:571 — activePrompt not cleared after post-admission observation failure; author defends as intentional fail-closed reuse boundary with test assertion (PromptAlreadyActiveException in assertStoppedPromptSettlesWithBlockedClose); requires maintainer judgment on whether this design choice is acceptable Not reviewed: coverage — agent prompts were paraphrased rather than passed verbatim from agent-prompt --roster; agents performed substantive work but check-coverage could not verify prompt fidelity. Not reviewed: chunk 10, chunk 9, chunk 11, chunk 2, chunk 15, chunk 7, chunk 3, chunk 21, chunk 12, chunk 5, chunk 4, chunk 20, chunk 13, chunk 14, chunk 18, chunk 17, chunk 8, chunk 19, chunk 6, chunk 16 — launched with a prompt that is not the one the CLI built. Not reviewed: Test coverage matrix (whole-diff), Agent 1b: Removed-behavior audit, Agent 1c: Cross-file tracer, Agent 7: Build & test verification, Invariant agent A: state, timers, collections — packages/sdk-java/qwencode/QWEN.md, Invariant agent B: counters, return values, error taxonomies — packages/sdk-java/qwencode/QWEN.md, Invariant agent C: config fields, early returns — packages/sdk-java/qwencode/QWEN.md — 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 reviewed: verification — the review posts findings, but no verifier was launched with a prompt this skill builds — they were ruled on, if at all, without the verdict bar its brief carries.

— qwen3.7-max via Qwen Code /review

Comment thread packages/acp-bridge/src/bridge.ts
doudouOUC and others added 7 commits July 23, 2026 15:07
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>
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>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Rebased this branch onto main at 01ee3bf921 and resolved the ACP prompt conflict by preserving both the trusted invocation context and prompt-admission cancellation. The existing invocation-context argument remains second; the new cancellation signal is appended as the third argument.

Validation:

  • packages/cli: 707 targeted ACP tests passed
  • Repository build and TypeScript typecheck passed
  • Java SDK Maven verify passed: 107 tests, 0 failures/errors, 5 skipped
  • Two post-fix self-audit passes were clean

The updated head is 0c3a6823e6; CI should restart after GitHub ingests the force-push.

@doudouOUC
doudouOUC force-pushed the agent/java-daemon-sdk-alpha branch from 4cef807 to 0c3a682 Compare July 23, 2026 07:28
@wenshao

wenshao commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Code Review — feat(sdk-java): Add daemon transport (#7463)

Verdict: strong, ship-worthy alpha. No blockers found. Deep review of the shared TypeScript reliability changes, the new Java transport (SSE/HTTP client, ~2.7k lines of tests), the release/CI automation, and the E2E harness surfaced only minor/latent items and a couple of semantic points worth confirming. This is unusually careful, uniformly fail-closed code.

Overview

Two things ship together:

  1. A Java 11 daemon transport added to the existing com.alibaba:qwencode-sdk artifact (0.1.0-alpha): thread-scoped sessions, admission-vs-terminal separation, resumable SSE from the admission watermark, strict framing/UTF‑8/JSON validation, typed + raw events, permission responses, cancellation, and a bounded promptText().
  2. Daemon/ACP reliability contracts the Java client depends on, closed in TypeScript (acp-bridge/bridge.ts, cli/.../acpAgent.ts, cli/.../session/Session.ts): a correlated terminal for every admitted prompt, deadlines covering queue+execution, queued-vs-running cancellation, admission-aware cancellation that waits for the targeted call to settle, and teardown ordering.

Strengths

  • Fail-closed everywhere. Strict SSE framing (SseReader), strict JSON with duplicate-key rejection (JsonSupport via Jackson STRICT_DUPLICATE_DETECTION), bounded response bodies (HttpSupport), compressed-stream rejection, gap detection, dedup, and "no reliable terminal ⇒ PromptOutcomeIndeterminateException" all prefer an explicit error over a partial success.
  • Correct on the hard invariants. Verified: the event cursor advances only after synchronous observer delivery (dedup → gap-check → dispatch → advance); stream InputStreams are closed on every 200/non-200/error path via an idempotent activeStream.compareAndSet gate; terminal correlation is mutually exclusive (outcomeClaimed under outcomeLock); admission-unknown deliberately keeps the session fail-closed and non-retried; UTF‑8 is byte-exact across chunk boundaries (0x0A never appears mid-multibyte).
  • Concurrency discipline. Every InterruptedException restores the interrupt flag; awaitAutomaticHeartbeat is a correct guarded wait()/notifyAll(); executor/slot accounting is balanced on all success and rejection paths; blocking waits are deadline- or attempt-bounded; close()/destroySession()/detach-once are idempotent.
  • Test coverage is excellent and mirrors nearly every concurrency/resource concern (fault injection for early terminals, fragmentation, replay dup, gaps, malformed frames, compression, reconnect exhaustion, observer failure, blocked cleanup, lost mutation responses, UTF‑8 limits), plus new bridge/CLI cancellation tests and a real-daemon E2E.
  • Conventions followed: craft/* extension-method naming, bilingual PR body + template, SHA-pinned actions, maven.compiler.release=11, Logback demoted to test scope with slf4j-api as the compile dep (matching the stated breaking change).

TypeScript reliability changes (highest-risk — shared by all daemon clients)

These are the changes I scrutinized most, since they touch cancellation paths used by the web-shell/ACP clients, not just the new Java SDK.

  • Dedup refactor is a genuine improvement. Replacing the boolean cancelBroadcast latch (which had to be reset at next-prompt-start) with a per-prompt cancelBroadcastPromptId removes a fragile reset step; new prompt ids never collide with old ones, so no reset is needed.
  • The cancel handshake is sound. forwardRunningPromptCancel memoizes on cancelForwardInitial (duplicate callers await, never resend), races the craft/cancelPendingPrompt extension against transport-close and the deadline, falls back to standard session/cancel on -32601/"not currently generating", and fences the next FIFO dispatch on cancelForwardDrain. Session.prompt's new admissionCancellation correctly aborts pendingSend at any phase, and releasePendingSend guards this.pendingPrompt === pendingSend so it can't clobber a newer prompt's controller (this also fixes a pre-existing dangling-pendingPrompt leak on the "cancelled during drain" path).
  • Removing the direct entry.connection.cancel({sessionId}) on the forward-failed path is a correctness fix, not just a refactor — the updated test asserts the late session-scoped cancel is no longer sent, so a stale cancel can't hit the queued successor.

⚠️ Behavior change worth confirming: cancelSession for a dispatched, running prompt now blocks until the prompt call settles (the agent handler awaits call.settled), instead of firing a fire-and-forget notification and returning. For the first-party agent (which honors the abort) this settles quickly. But if an ACP agent ignores the abort and no per-prompt deadline is configured, cancelForwardDeadline is undefined and POST /cancel is bounded only by transport-close — i.e. it can hang. Please confirm (a) serve configures a prompt deadline by default, or (b) the added cancel latency is acceptable for existing HTTP clients (web-shell) in the common case.

Java transport — minor / latent findings

  1. Latent lock-ordering inversion (minor, safe today). DaemonClient.createSession holds the client lifecycleLock while calling startAutomaticHeartbeat(), which takes the session lifecycleLock. The reverse order (session lock → sendSessionMutation → client lock) exists for heartbeat/cancelActivePrompt/respondToPermission. It's not a deadlock only because createSession targets a freshly-constructed, unpublished session no other thread can lock. Fragile — a future change touching an already-registered session's lock under the client lock would deadlock. Suggest calling startAutomaticHeartbeat() after releasing the client lock, or a comment pinning the invariant.
  2. Manual session mutations hold the session lock across blocking HTTP I/O (heartbeat/cancelActivePrompt/respondToPermission). A close()/destroySession() can therefore block up to requestTimeout behind an in-flight manual heartbeat — bounded, but asymmetric with the automatic heartbeat path, which deliberately runs the network call outside the lock.
  3. SseReader trailing-empty-data join deviates from the WHATWG SSE dispatch algorithm (immaterial for the daemon's single-line JSON envelopes; the trailing-\n strip is effectively dead code for well-formed input). Fine to leave, worth a comment.
  4. httpExecutor sizing under high SSE concurrency — a stress test with many simultaneous open streams (up to maximumConcurrentPrompts, default 32) against the fixed min(16, …)-thread pool + ArrayBlockingQueue(256) would be worth running before GA; I couldn't rule out starvation from source alone (depends on JDK HttpClient internals).

Nit: parseId/parseRetry use Character.isDigit (accepts non-ASCII digits); it still fails closed via Long.parseLong, but the resulting "outside the long range" message is misleading for a Unicode-digit input.

CI / release / build

The release workflow is more hardened than the repo's existing release-sdk*.yml: SHA-pinned actions, persist-credentials: false, secrets referenced by env-var name (never echoed; no set -x), and signing/publishing doubly gated by if: !inputs.dry_run and a protected production-release environment. Require protected main + the checked_out_sha == github.sha check make the trigger effectively non-bypassable; no pull_request_target or untrusted interpolation into run:. Minor items:

  • Two run: blocks (create/push tag; create GH Release) omit set -euo pipefail used elsewhere — a failure-surfacing nit.
  • "Verify Maven Central availability" polls only 5 min (30×10s); Portal→repo1 propagation often takes longer, so it can false-fail after a successful (irreversible) publish. Recoverable via the resume path, but expect spurious red releases — consider a longer/backoff budget.
  • The E2E mvn child and the workflow jobs have no timeout (timeout-minutes unset), so a hung mvn runs to the ~6h job default. Add a job timeout / wrap the mvn await.
  • E2E script has no SIGINT/SIGTERM handler, so a cancelled CI job orphans qwen serve/mvn (teardown otherwise clean: SIGTERM→SIGKILL, env scrubbed, temp isolated, fails loud).
  • Hardening: scope the Maven/GPG secrets to the production-release environment (not repo-wide) so dry-run is provably unable to touch signing material; and confirm the workflow's secret names + server-id: central match the pom's <publishingServerId>. Note pom.xml sets autoPublish=true + waitUntil=published, so a production mvn deploy publishes irreversibly — the dry-run gating (verified sound) is what protects this.

Risks / scope

  • Breaking change: the whole artifact moves min Java 8 → 11 and drops the Logback runtime dep (consumers supply their own SLF4J backend). Documented; Java 8 users stay on 0.0.3-alpha; coordinates unchanged. Make sure this lands prominently in release notes.
  • Cross-package blast radius: the cancellation-path edits affect every daemon/ACP client. Coverage is strong, but the cancel-semantics change above deserves an explicit maintainer sign-off (the PR itself flags this).

Nice work — this is a lot of hard protocol code held to a high bar.

中文版本

代码审查 — feat(sdk-java): Add daemon transport (#7463)

结论:高质量的 alpha,可以合入。未发现阻塞性问题。 对共享的 TypeScript 可靠性改动、新的 Java transport(SSE/HTTP 客户端,约 2.7k 行测试)、发布/CI 自动化以及 E2E 工具做了深入审查,只发现少量次要/潜在问题,以及两处值得确认的语义点。整体代码非常严谨,一致地 fail-closed。

概述

  • Java 11 daemon transport:thread scope 会话、admission 与 terminal 分离、从 watermark 恢复 SSE、严格的 framing/UTF‑8/JSON 校验、类型化 + 原始事件、权限响应、取消、有界 promptText()
  • daemon/ACP 可靠性契约bridge.tsacpAgent.tssession/Session.ts):每个 admission 的 prompt 都有关联终态、deadline 覆盖排队+执行、区分 queued/running 取消、admission-aware 取消会等待目标调用结算、teardown 顺序。

优点

  • 全链路 fail-closed(严格 SSE、重复键拒绝的 JSON、有界响应体、压缩流拒绝、gap 检测、去重、无可靠终态即抛异常)。
  • 关键不变式正确:游标仅在 observer 同步交付后推进;所有路径幂等关闭流;终态关联互斥;admission-unknown 保持 fail-closed 不重试;UTF‑8 跨块字节精确。
  • 并发规范:所有 InterruptedException 恢复中断标志;awaitAutomaticHeartbeat 是正确的 guarded wait/notify;线程池/slot 记账平衡;阻塞等待均有界;close/destroy/detach-once 幂等。
  • 测试覆盖非常完善(大量故障注入)+ 新增取消测试 + 真实 daemon E2E。
  • 遵循约定:craft/* 命名、双语 PR、SHA 固定 actions、compiler.release=11、Logback 降为 test scope。

TypeScript 可靠性改动(最高风险 — 所有 daemon 客户端共享)

  • ✅ 去重从 boolean latch 改为按 prompt id 记录,去掉了脆弱的“下一个 prompt 重置”步骤。
  • ✅ 取消握手正确:forwardRunningPromptCancel 记忆化、竞速 deadline/传输关闭、-32601 回退标准 cancel、用 cancelForwardDrain 栅栏化下一个 FIFO 派发;Session.promptadmissionCancellation 正确中止 pendingSendreleasePendingSendthis.pendingPrompt === pendingSend 守卫避免覆盖新 prompt(同时修复了旧的 pendingPrompt 悬挂泄漏)。
  • ✅ 移除 forward-failed 路径上的直接 connection.cancel 是正确性修复(避免过期 cancel 命中排队中的后继 prompt)。

⚠️ 需确认的行为变化:对已派发的运行中 prompt,cancelSession 现在会阻塞直到该 prompt 调用结算(不再是 fire-and-forget)。首方 agent 会很快结算;但若某 ACP agent 忽略 abort 且未配置 per-prompt deadlinePOST /cancel 只受传输关闭约束,可能挂起。请确认 serve 默认配置了 deadline,或该延迟对现有 HTTP 客户端(web-shell)可接受。

Java transport — 次要/潜在问题

  1. 潜在锁顺序反转(当前安全)createSession 持有 client 锁时调用 startAutomaticHeartbeat()(获取 session 锁),而 session mutation 是反向(session 锁 → client 锁)。当前仅因目标是刚构造、未发布的 session 才不死锁,较脆弱。建议在释放 client 锁后再调用,或加注释固化不变式。
  2. 手动 session mutation 持 session 锁跨阻塞 HTTP I/Oclose/destroy 可能被在途手动 heartbeat 阻塞至多 requestTimeout(有界,但与自动 heartbeat 路径不对称)。
  3. SseReader 对结尾空 data 字段的处理偏离 WHATWG(对单行 JSON envelope 无影响)。
  4. 高并发 SSE 下 httpExecutor 容量:建议 GA 前做高并发压力测试。

小 nit:parseId/parseRetryCharacter.isDigit(仍 fail closed,但错误信息对 Unicode 数字有误导)。

CI / 发布 / 构建

发布 workflow 比现有 release-sdk*.yml 更硬化(SHA 固定、persist-credentials: false、secret 仅按名引用、双重 dry-run 门控、main 守卫不可绕过)。次要项:

  • 两处 run:set -euo pipefail
  • Maven Central 可用性轮询仅 5 分钟,可能在成功(不可逆)发布后误报失败(可通过 resume 恢复)。
  • E2E 的 mvn 子进程与 job 均无 timeout-minutes(挂起会跑到约 6h 默认值)。
  • E2E 脚本无 SIGINT 处理,CI 取消会遗留 qwen serve/mvn
  • 加固:将 Maven/GPG secret 限定到 production-release 环境;确认 secret 名与 server-id: central 同 pom 的 <publishingServerId> 一致。注意 pom 设了 autoPublish=true + waitUntil=published,生产 mvn deploy 会不可逆发布,靠 dry-run 门控保护(已验证可靠)。

风险 / 范围

  • 破坏性变更:整个 artifact 最低 Java 8→11 并移除 Logback runtime 依赖;已文档化,Java 8 用户留在 0.0.3-alpha,坐标不变。请在发布说明中醒目标注。
  • 跨 package 影响面:取消路径改动影响所有 daemon/ACP 客户端;覆盖充分,但上面的取消语义变化建议 maintainer 显式签核(PR 本身也已提示)。

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No issues found. LGTM! ✅

— qwen3.7-max via Qwen Code /review

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

@wenshao Thanks for the thorough review. I verified the cancellation point against the current code:

  • qwen serve intentionally leaves the prompt deadline unlimited when neither --prompt-deadline-ms/QWEN_SERVE_PROMPT_DEADLINE_MS nor request deadlineMs is set.
  • The first-party agent propagates the targeted abort into pendingSend and normally settles promptly.
  • A custom ACP agent that ignores abort can therefore keep the server-side cancel forward pending when no prompt deadline exists. The Java caller itself remains bounded by its mutation requestTimeout (30 seconds by default), after which the outcome is reported as unknown, but the server-side operation may remain pending until prompt settlement or transport close.

I am not adding an arbitrary cancel timeout in this PR: safely expiring it requires an explicit contract choice between preserving the per-session FIFO fence, allowing the documented deadline-style overlapping recovery, or tearing down a possibly shared runtime. Operators that require bounded cancellation should configure a server or per-prompt deadline. A default deadline/cancel-timeout policy needs maintainer design follow-up rather than an implicit alpha behavior change.

Following the repository rule for a PR that has already gone through roughly five review rounds, I am recording the remaining non-Critical items without expanding this diff:

Item Disposition
Current-safe client/session lock-order invariant and manual mutation lock scope Defer to Java SDK concurrency cleanup before GA
WHATWG trailing-empty-data nuance and Unicode-digit error wording Defer; daemon JSON envelopes are unaffected and parsing remains fail-closed
High-concurrency HttpClient executor stress coverage Add before GA
Workflow/job timeouts, signal cleanup, Maven Central polling budget, and shell hardening Defer to release-workflow hardening before the first production publish
Release environment secret scoping Repository environment configuration; maintainers should confirm before production release

The Java 11 migration and SLF4J/Logback change are already called out in the SDK documentation and release guidance. No blocker was identified, and the current SHA is fully green.

@wenshao wenshao 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.

Reviewed. Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally (pre-existing @xterm/headless environment issue). Not reviewed: build-and-test — Test (macos-latest, Node 22.x) was skipped in CI. Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI. Not reviewed: build-and-test — Java build/test could not run (no Maven on runner). Not reviewed: chunk 19, chunk 11, chunk 20, chunk 5, chunk 15, chunk 1, This PR adds a Java 11 daemon transport to the qwencode-s..., chunk 12, chunk 2, chunk 18, chunk 16, You are review agent verify — Verification agent., chunk 8, chunk 7, chunk 6, chunk 10, chunk 14, chunk 21, chunk 9, chunk 3, chunk 4, chunk 13, chunk 17, You are review agent reverse-audit — Reverse audit agen... — pointed at diff lines it never opened: it made tool calls, but none of them read the diff.

— qwen3.8-max-preview via Qwen Code /review

@wenshao

wenshao commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Code Review — PR #7463 feat(sdk-java): Add daemon transport

Reviewed at head 0c3a682 (fix(cli): preserve prompt cancellation during context propagation). Method: static review of the full diff + head-of-branch file contents, cross-checked against CI status (all Java 11/17/21 matrix jobs, real-daemon E2E, and Node suites are green) and the earlier local-verification reports on this thread. I did not re-run the build myself; the findings below were each verified against the actual source at the PR head.

Overview

Adds a Java 11 daemon HTTP/SSE transport to the existing com.alibaba:qwencode-sdk artifact (no new Maven coordinate) and bumps to 0.1.0-alpha. The transport does a non-retried POST admission, then resumable SSE from the returned watermark, with strict UTF-8/JSON/SSE validation, after-delivery cursor advancement, replay dedup, gap rejection, typed *Unknown/Indeterminate exceptions, and fail-closed terminal correlation. Alongside it, the daemon/ACP side closes the reliability contracts the client depends on: an acknowledged admission-aware prompt cancellation handshake (craft/cancelPendingPrompt), a FIFO cancel-drain fence, and per-prompt prompt_cancelled dedup. Plus docs, a Java 11 migration note, cross-platform CI, a real-daemon E2E harness, and a protected Maven Central release workflow.

Verdict

Strong, merge-worthy alpha. This is unusually careful, defensively-written transport code, and the load-bearing guarantees it advertises hold up under adversarial interleavings (see below). I found one Medium code defect (a byte-accounting under-count that weakens the promptText memory bound on malformed content), a few Low-severity strictness/liveness edges, and one release-workflow precondition to confirm before the first production publish. Nothing High-severity.

What's well done (verified, not just asserted)

I specifically tried to break these on the SSE engine and could not:

  • After-delivery cursor advance + dedup + gap rejection. cursor is assigned only after processEvent delivers; id <= cursor is dropped; any id != cursor+1 throws → indeterminate. A daemon that ignores Last-Event-ID and replays from id 1 on reconnect is correctly de-duplicated with no double delivery.
  • No partial-success. promptText returns only on a claimed turn_complete terminal (claimTerminal under outcomeLock); any outcome without a claimed terminal is PromptOutcomeIndeterminateException — there is no truncation-as-success path.
  • Compressed / wrong-content-type streams can't be bypassed (validateSseHeaders runs before any body read; a covertly-gzipped identity stream fails strict UTF-8 decode).
  • Terminal→prompt correlation can't match the wrong prompt (single active prompt/session, belongsTo throws on envelope-vs-data promptId conflict, stale terminals fall below the cursor).
  • Lock ordering / slot accounting / stream close are balanced on every path; the one inverted CLIENT→SESSION lock path only touches an unpublished session's lock and cannot deadlock.

On the TS side, the new bridge cancellation path is well-tested: admission-cancel-before-dispatch, cancel-during-child-admission (retry loop), the FIFO drain fence, channel-crash-during-handshake (releases the fence via transport-close), custom-agent -32601 fallback to standard session/cancel, and per-prompt prompt_cancelled dedup all have targeted tests.


Findings

1. [Medium] promptText UTF-8 byte limit under-counts unpaired surrogates → ~3× overrun on malformed content
DaemonSessionClient.java, TextCollector.utf8Bytes / finish (~L1146–1192).

utf8Bytes counts a lone high surrogate, a lone low surrogate, and a deferred pending high surrogate as 1 byte each. But when that text is realized as UTF-8 (String.getBytes(UTF_8)), Java maps every unpaired surrogate to U+FFFD = 3 bytes. Raw lone surrogates can't arrive over the strict-UTF-8 transport, but the JSON string value can carry \uD800-style escapes, and the strict decoder (JsonSupport enables only STRICT_DUPLICATE_DETECTION) does not reject them — Jackson's getText() yields a lone-surrogate char that reaches onText. So a daemon or a model emitting \uXXXX escapes controls this.

Scenario: daemon streams agent_message_chunk text of \uD800\uD800…. Each char counts as ~1 byte instead of 3, so a 4 MiB maximumBytes accumulates ~4M surrogate chars (~12 MiB of realized UTF-8, ~8 MiB char[]) before PromptContentLimitException fires. The overrun is a bounded constant factor (~3×), and for all well-formed text the estimate is exact — so this only bites on malformed/escaped content. Still, it defeats the documented "enforces a UTF-8 byte limit" guarantee. Fix: count unpaired surrogates as 3 bytes (their U+FFFD encoding) in utf8Bytes/finish.

2. [Low] SSE idle watchdog keys off byte arrival, not frame progress
SseReader.next blank-line branch + DaemonSessionClient.scheduleIdleWatchdog / ActivityInputStream.

The idle watchdog only trips when no bytes arrive for sseIdleTimeout. A daemon that streams an endless run of blank lines or :comment lines keeps ActivityInputStream firing (bytes are arriving) so the idle watchdog never trips, while next() never returns a frame and the per-frame size cap is never hit (each blank line resets frameBytes). The reader thread is then pinned until the full observation deadline (default 30 min) → indeterminate. It's bounded and fail-closed, but a single misbehaving daemon can tie up a worker for the entire observation window and evade the idle timeout. Consider counting consecutive no-frame-progress time, not just byte silence.

3. [Low] parseId/parseRetry accept non-ASCII Unicode digits
SseReader.java (~L160–191). value.chars().allMatch(Character::isDigit) + Long.parseLong both accept Unicode decimal digits (fullwidth 0-9, Arabic-Indic). The SSE id/retry contract is ASCII decimal. Practically harmless (the envelope-id == frame-id equality check downstream blocks any real mismatch), but looser than the "positive integer" intent. Restrict to [0-9].

4. [Low — precondition, not a diff defect] Confirm release secrets are environment-scoped
.github/workflows/release-sdk-java.yml. The workflow is genuinely well-built (SHA-verifies the checkout equals the dispatch SHA, idempotent tag/artifact preflight, fail-closed Central probe, set -euo pipefail, inputs passed via env: not interpolated into run:, persist-credentials: false, step-scoped GH_TOKEN, cancel-in-progress: false, dry_run defaults true). The Require protected main step + github.repository gate are defense-in-depth but are not the real security boundary — workflow_dispatch runs the workflow as defined on the dispatched ref, so a write-access actor could dispatch a modified copy. The only thing that then bounds signing-key exfiltration is that MAVEN_GPG_PRIVATE_KEY, MAVEN_GPG_PASSPHRASE, CENTRAL_USERNAME, CENTRAL_PASSWORD are readable only when the job targets the reviewer-gated production-release Environment. Please confirm all four are stored as Environment secrets on production-release with required reviewers, not repo/org secrets, and treat that as a documented merge precondition.

5. [Low] No timeout-minutes on CI/release jobs; E2E harness has no wall-clock or signal guard
Neither workflow sets timeout-minutes, and scripts/run-java-daemon-sdk-e2e.ts awaits the mvn child with no bound (the deadline/cancel scenarios deliberately hang the model via new Promise(() => {}) and rely solely on --prompt-deadline-ms). A hung Java test or slow Maven fetch hangs the run up to the 6h default. The harness also has no SIGINT/SIGTERM handler, so a CI cancel can orphan the qwen serve daemon and the mvn child (cleanup lives only in finally). Add timeout-minutes and a signal handler / wall-clock guard.

6. [Informational] Two TS cancellation edges — already raised and adjudicated on this thread; my read converges

  • The FIFO cancel-drain fence (drainCancelForwarding awaiting pending.cancelForwardDrain) has no acknowledgement-only timeout. This is the earlier §5.1 item: it's a deliberate, documented choice (a timeout would let a late session-scoped cancel reach the queued successor). A provider/tool that ignores its AbortSignal with no deadline and an open transport can fence the session's queue — recovery is to destroy the session, per docs/design/java-daemon-sdk-alpha.md. The channel-crash test confirms transport-close still releases the fence. Acceptable as an alpha contract.
  • broadcastPromptCancelledOnce now keys dedup on cancelBroadcastPromptId and skips both the suppression check and the recording when promptId === undefined — so the old boolean latch's dedup is lost for an id-less turn (entry.activePromptId ?? runningPrompt?.promptId could be undefined). The earlier §5.2 assessment (low: at most one redundant advisory prompt_cancelled, no state error) holds. Worth a one-line guard if id-less running prompts are reachable.

Note the deliberate behavior change (bridge.ts): the fire-and-forget entry.connection.cancel({ sessionId }) after prompt abort was removed because a session-scoped cancel could hit a queued successor — correct given the move to prompt-targeted cancellation (test at the former cancelSpy assertion now asserts not called).

Test coverage

Good. New bridge/Session/acpAgent tests cover the cancellation contract directly, and the Java suite injects SSE fragmentation, replay/duplicates/gaps, conflicting prompt IDs, compressed responses, stalled bodies, resync, observer failures, terminal absence, and ambiguous mutations. One carry-over caveat worth restating in the PR: the real-daemon E2E uses a mock model — it validates transport/protocol correctness (routing, SSE ordering, permission, cancel/deadline terminals), not real model-response parsing, token counting, or tool execution.

Minor

  • fastjson2 2.0.60 parses daemon responses; autoType is off by default and the daemon is loopback + auth-gated, so risk is low — just keep it on the watch-list and ensure no SupportAutoType path is ever added. jackson-core 2.22.0 is intentional (strict decode, distinct from fastjson2 encode) and CI proves it resolves on Central.

Overall: high-quality, well-documented work. Recommend addressing #1 (surrogate accounting), confirming #4 (secret scoping) before the first production release, and treating #2/#3/#5 as low-priority cleanups.

中文说明

在 head 0c3a682 上评审。方法:对完整 diff + PR head 源码做静态评审,并交叉核对 CI 状态(Java 11/17/21 矩阵、真实 daemon E2E、Node 测试全绿)与本 PR 线程上此前的本地验证报告。我未自行重跑构建;下述每条结论都已对照 PR head 的真实源码逐一核实。

概述:在现有 com.alibaba:qwencode-sdk 制品中新增 Java 11 daemon HTTP/SSE transport(不新增 Maven 坐标),版本升到 0.1.0-alpha。传输层:不重试的 POST admission → 从 watermark 恢复的 SSE,严格 UTF-8/JSON/SSE 校验、仅在投递后推进游标、回放去重、缺口拒绝、类型化 *Unknown/Indeterminate 异常、fail-closed 终态关联。同时在 daemon/ACP 侧补齐可靠性契约:acknowledged 的 admission-aware 取消握手(craft/cancelPendingPrompt)、FIFO cancel-drain 栅栏、按 prompt 去重的 prompt_cancelled。另含文档、Java 11 迁移说明、跨平台 CI、真实 daemon E2E 工具与受保护的 Maven Central 发布流程。

结论质量很高、可以合并的 alpha。 SSE 引擎的核心保证(投递后推进游标、回放去重、缺口拒绝、压缩流拒绝、终态关联、无部分成功、锁顺序、slot 记账)在对抗性交错下均成立。发现 1 个 Medium 代码缺陷(malformed 内容下 promptText 字节上限少算,约 3× 溢出)、几个 Low 级严格性/活性边界,以及 1 个首次生产发布前需确认的发布流程前置条件。无 High 级问题。

主要发现

  1. [Medium] TextCollector.utf8Bytes/finish(约 L1146–1192)把落单代理项(lone surrogate)按 1 字节计,但 getBytes(UTF_8) 会把每个未配对代理项变成 U+FFFD=3 字节。严格解码器(JsonSupport 仅开启 STRICT_DUPLICATE_DETECTION)不拒绝 \uD800 转义,getText() 会把落单代理项交给 onText。因此 daemon/模型可用 \uXXXX 触发:4 MiB 上限实际可累积约 12 MiB。仅对 malformed 内容生效、溢出为有界常数(约 3×),但违背"UTF-8 字节上限"的文档保证。建议 utf8Bytes/finish 把未配对代理项按 3 字节计。
  2. [Low] SSE 空闲看门狗只看"有无字节到达",不看"帧是否推进"。持续发送空行/:comment 会让 ActivityInputStream 一直触发而 next() 永不产帧,reader 线程被占用直到 observation deadline(默认 30 分钟)。有界、fail-closed,但会占用 worker 整个观测窗口。
  3. [Low] parseId/parseRetry 接受非 ASCII Unicode 数字(Character::isDigit+parseLong)。因下游 envelope-id==frame-id 相等校验,实际无害,但比"正整数"意图更宽松。
  4. [Low — 前置条件,非 diff 缺陷] release-sdk-java.yml 写得很好,但 "Require protected main" 与 github.repository 门禁不是真正的安全边界——workflow_dispatch 按所选 ref 的定义运行。真正约束密钥外泄的是四个 MAVEN_*/CENTRAL_* 密钥仅在 job 目标为受审阅门禁的 production-release 环境时可读。请确认这四个密钥是 production-releaseEnvironment secrets 且配置了 required reviewers,而非 repo/org secrets,并作为合并前置条件。
  5. [Low] 两个 workflow 均未设 timeout-minutes,E2E 脚本对 mvn 子进程无 wall-clock 上限、无 SIGINT/SIGTERM 处理(清理仅在 finally),CI 取消可能残留 qwen servemvn 子进程。建议补上超时与信号处理。
  6. [信息] 两个 TS 取消边界此前已在本线程讨论并定性,我的独立评审与其一致:cancel-drain 栅栏无 ack-only 超时是有意的、已在设计文档记录的选择;cancelBroadcastPromptIdpromptId===undefined 时跳过去重(此前评估为低影响,仅一条冗余 advisory 事件)。另:移除 prompt abort 后的 fire-and-forget connection.cancel({sessionId}) 是正确的(避免误伤排队中的后继 prompt)。

测试覆盖:良好。提醒一点(建议在 PR 中重申):真实 daemon E2E 使用 mock 模型,验证的是传输/协议正确性,不覆盖真实模型响应解析、token 计数与工具执行。

次要fastjson2 2.0.60 建议保持关注(勿引入 SupportAutoType);jackson-core 2.22.0 用于严格解码,CI 已证明可从 Central 解析。

总体:文档与实现都很扎实。建议处理 #1,首次生产发布前确认 #4#2/#3/#5 作为低优先清理。

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough review. I independently checked each new item at 0c3a682.

  1. Not taking the surrogate-accounting change. The premise is not true for the Java encoding operation used by the contract/tests. On Java 21, "\uD800".getBytes(StandardCharsets.UTF_8) is one byte ([63]), and StandardCharsets.UTF_8.newEncoder().replacement() is also [63]; Java 11/17 use the same encoder replacement. The existing regression tests intentionally compare bounded partial text with getBytes(StandardCharsets.UTF_8) and therefore correctly count an unpaired surrogate as one realized byte. Changing it to three would count a hypothetical U+FFFD normalization that this API does not perform and would make the bound stricter than its stated Java UTF-8 realization. Valid surrogate pairs remain correctly counted as four bytes across chunks.

  2. Confirmed: the production release environment is not ready. The GitHub API currently reports production-release with protection_rules: []; its environment-secret list contains only NPM_TOKEN. The four Java release secrets (MAVEN_GPG_PRIVATE_KEY, MAVEN_GPG_PASSPHRASE, CENTRAL_USERNAME, CENTRAL_PASSWORD) are not environment-scoped there, and the repository-secret list does not contain them either. I cannot inspect organization-secret scope without org-admin permission, but organization/repository scope would not satisfy the required reviewer boundary in any case. A maintainer must configure reviewer-gated environment protection and those four environment secrets before the first non-dry-run publish. Because production-release is shared by other release workflows, maintainers should decide whether to protect the shared environment or give Java publishing a dedicated protected environment.

The frame-progress watchdog, ASCII-only numeric parsing, and workflow/E2E wall-clock guards are valid Low-priority hardening ideas. This PR has already passed roughly five review rounds, so per the repository contribution rule I am deferring non-Critical scope growth; they do not affect the alpha transport's fail-closed correctness. No code change is warranted from this round.

@doudouOUC
doudouOUC requested a review from wenshao July 23, 2026 12:17

@yiliang114 yiliang114 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

@doudouOUC
doudouOUC added this pull request to the merge queue Jul 23, 2026
Merged via the queue into QwenLM:main with commit e7097d0 Jul 23, 2026
73 checks passed
@doudouOUC
doudouOUC deleted the agent/java-daemon-sdk-alpha branch July 23, 2026 12:45
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