Skip to content

fix(daemon): Reject stale prompt client admission#5784

Merged
wenshao merged 3 commits into
QwenLM:mainfrom
doudouOUC:codex/fix-stale-client-prompt-admission
Jun 24, 2026
Merged

fix(daemon): Reject stale prompt client admission#5784
wenshao merged 3 commits into
QwenLM:mainfrom
doudouOUC:codex/fix-stale-client-prompt-admission

Conversation

@doudouOUC

Copy link
Copy Markdown
Collaborator

What this PR does

This PR makes stale or unregistered prompt client ids fail during prompt admission instead of becoming asynchronous prompt failures after the HTTP route has already accepted the request. The bridge now treats invalid prompt client ids the same way it already treats queue-full and pre-aborted prompt admission failures, and the daemon route returns the existing invalid_client_id response shape without a promptId.

The tests cover the bridge admission contract, the daemon /prompt response shape, and the SDK non-blocking prompt error mapping.

Why it's needed

A daemon restart or session reload can issue a new client id while a UI still has an older one. Before this change, /prompt could return 202 Accepted for that stale client id, then the bridge would reject asynchronously before any turn was registered, so no turn_complete or turn_error event would ever arrive for the accepted prompt. Clients that rely on the returned promptId could stay in a processing state indefinitely.

Returning 400 invalid_client_id before acceptance keeps the client out of the pending-prompt path and lets existing UI error handling clear the active prompt state.

Reviewer Test Plan

How to verify

Run the targeted prompt admission tests and confirm stale prompt client ids return 400 invalid_client_id without a promptId, while true asynchronous prompt failures still return 202 and rely on turn events. Also confirm the bridge rejects unregistered prompt clients synchronously and the SDK maps the response to DaemonHttpError instead of creating an accepted non-blocking prompt.

Evidence (Before & After)

Before: a stale prompt client id could be accepted with 202, then fail asynchronously before any terminal turn event was emitted, leaving the client waiting on a prompt id that would never complete.

After: the route-level regression test verifies stale prompt client ids return 400 invalid_client_id with no promptId, while the existing asynchronous failure test still verifies true turn failures remain on the 202 path.

Tested on

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

Environment (optional)

Node.js v22.22.3. Local targeted Vitest coverage was run for packages/acp-bridge, packages/cli, and packages/sdk-typescript.

Risk & Scope

  • Main risk or tradeoff: callers that directly invoked sendPrompt with an invalid client id and expected a rejected Promise now receive a synchronous admission throw, matching the documented non-async prompt admission contract.
  • Not validated / out of scope: this PR does not persist client ids across daemon restarts or automatically retry prompts after a stale client id failure.
  • Breaking changes / migration notes: no wire-shape migration; the daemon reuses the existing invalid_client_id response shape.

Linked Issues

N/A

中文说明

What this PR does

这个 PR 将 stale 或未注册的 prompt client id 改为在 prompt admission 阶段失败,而不是在 HTTP 路由已经接受请求后再异步失败。bridge 现在把无效 prompt client id 视为和队列已满、预先 abort 的 prompt 一样的 admission failure,daemon 路由会返回既有的 invalid_client_id 响应,并且不返回 promptId

测试覆盖了 bridge admission 契约、daemon /prompt 响应形状,以及 SDK 非阻塞 prompt 的错误映射。

Why it's needed

daemon 重启或 session reload 后可能会签发新的 client id,而 UI 仍然持有旧 client id。修改前,/prompt 可能会对这个 stale client id 返回 202 Accepted,随后 bridge 在任何 turn 注册之前异步拒绝,因此这个已接受 prompt 不会收到 turn_completeturn_error 事件。依赖返回 promptId 的客户端可能会一直停留在处理中状态。

在接受请求前返回 400 invalid_client_id,可以让客户端不进入 pending-prompt 路径,并让现有 UI 错误处理清理 active prompt 状态。

Reviewer Test Plan

How to verify

运行针对 prompt admission 的测试,确认 stale prompt client id 返回 400 invalid_client_id 且没有 promptId,同时真实的异步 prompt 失败仍然返回 202 并依赖 turn 事件。还需要确认 bridge 会同步拒绝未注册 prompt client,SDK 会把响应映射为 DaemonHttpError,而不是创建一个已接受的非阻塞 prompt。

Evidence (Before & After)

Before:stale prompt client id 可能先被 202 接受,然后在任何终止 turn 事件发出前异步失败,导致客户端等待一个永远不会完成的 prompt id。

After:路由层回归测试验证 stale prompt client id 返回 400 invalid_client_id 且没有 promptId,同时已有异步失败测试仍验证真实 turn 失败保持在 202 路径。

Tested on

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

Environment (optional)

Node.js v22.22.3。本地针对 packages/acp-bridgepackages/clipackages/sdk-typescript 运行了相关 Vitest 测试。

Risk & Scope

  • Main risk or tradeoff:如果有调用方直接用无效 client id 调用 sendPrompt 并期望拿到 rejected Promise,现在会收到同步 admission throw;这符合已文档化的非 async prompt admission 契约。
  • Not validated / out of scope:这个 PR 不做跨 daemon 重启的 client id 持久化,也不会在 stale client id 失败后自动重试 prompt。
  • Breaking changes / migration notes:没有 wire-shape 迁移;daemon 复用既有的 invalid_client_id 响应形状。

Linked Issues

N/A

Return invalid prompt client ids as synchronous admission failures so daemon clients do not wait forever after a stale client id is rejected.

Co-authored-by: Qwen-Coder <[email protected]>
@doudouOUC
doudouOUC marked this pull request as ready for review June 23, 2026 16:52
Copilot AI review requested due to automatic review settings June 23, 2026 16:52
@doudouOUC doudouOUC self-assigned this Jun 23, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓ — all required sections present including bilingual description.

On direction: this solves a real problem — stale client IDs after daemon restarts cause accepted prompts to silently fail, leaving clients stuck waiting for events that never arrive. The fix is squarely within the daemon reliability work the project has been doing (session management, prompt admission contracts). Aligned.

On approach: the scope is tight and well-contained. Three production files changed with a total of ~10 lines of logic, plus three test files adding/updating coverage across bridge, daemon route, and SDK. The core insight is simple: resolveTrustedClientId already throws synchronously, and the existing try/catch → Promise.reject wrapper was the only thing preventing the synchronous throw from reaching the route's catch block. Removing that wrapper aligns the behavior with the documented admission contract (non-async, synchronous admission failures). This is the minimal change needed.

One observation: the PR also adds a daemonLog.warn entry for InvalidClientIdError in the route handler, matching the existing PromptQueueFullError logging pattern — reasonable for debugging stale client issues in production.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓ — 所有必需章节齐全,包括双语说明。

方向:这解决了一个真实问题 —— daemon 重启后 stale client ID 会导致已接受的 prompt 静默失败,客户端一直等待永远不会到达的事件。修复属于项目一直在做的 daemon 可靠性工作范围(session 管理、prompt admission 契约)。对齐。

方案:范围紧凑且克制。3 个生产文件共约 10 行逻辑变更,加上 3 个测试文件在 bridge、daemon 路由和 SDK 层面增加/更新覆盖。核心洞察很简单:resolveTrustedClientId 本身已经同步 throw,现有的 try/catch → Promise.reject 包装是唯一阻止同步 throw 到达路由 catch block 的东西。去掉这个包装使行为与文档化的 admission 契约一致(非 async,同步 admission 失败)。这是所需的最小改动。

一个观察:PR 还在路由处理器中为 InvalidClientIdError 添加了 daemonLog.warn,与现有的 PromptQueueFullError 日志模式一致 —— 对生产环境中调试 stale client 问题有帮助。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal before reading the diff: the bug is that resolveTrustedClientId throws synchronously but the sendPrompt method wraps it in try/catch → Promise.reject, making the error asynchronous. The fix should remove the wrapper so the synchronous throw propagates to the route handler's catch block, which already knows how to map InvalidClientIdError to 400 invalid_client_id. The sendPrompt type docs should also be updated to list InvalidClientIdError alongside the existing PromptQueueFullError.

The PR's approach matches this exactly. No simpler path missed.

Review findings:

  • No critical blockers. The logic change is correct: removing the try/catch → Promise.reject wrapper around resolveTrustedClientId makes InvalidClientIdError throw synchronously, matching the non-async admission contract already documented in bridgeTypes.ts.
  • The existing sendBridgeError in server.ts (line ~5640) already handles InvalidClientIdError400 { code: 'invalid_client_id', sessionId, clientId } without a promptId, so no route-level changes were needed beyond adding the daemonLog.warn entry.
  • Test changes are thorough: bridge tests updated from rejects.toBeInstanceOf to toThrow (synchronous), new server route test verifies the 400 response shape and daemon log, new SDK tests verify both non-blocking and blocking prompt error mapping plus prompt slot release.
  • The promptId included in the daemonLog.warn payload is intentional for debugging correlation, even though the HTTP response body omits it. Consistent with the PromptQueueFullError logging pattern.

Reuse check: no new utility code or abstractions introduced. The PR reuses existing error types (InvalidClientIdError), existing response shape (invalid_client_id), and existing logging pattern (daemonLog.warn). Clean.

Test Results

All existing tests pass, plus 7 new/updated test assertions across 3 packages:

Package Tests Result
acp-bridge 296 ✅ all pass
cli/server 517 (+1 new) ✅ all pass
sdk-typescript 168 (+2 new) ✅ all pass

Before/After

Before (installed build — curl against running daemon)

Created a session, then sent a prompt with a stale client ID:

$ curl -s -w '\nHTTP_STATUS:%{http_code}' \
  -X POST http://127.0.0.1:14170/session/46b8b7c8-.../prompt \
  -H 'Content-Type: application/json' \
  -H 'X-Qwen-Client-Id: stale-client-123' \
  -d '{"prompt":[{"type":"text","text":"hello"}]}'

{"promptId":"31ae264e-49d9-49ad-8778-c961ceb8e155","lastEventId":1}
HTTP_STATUS:202

Bug confirmed: stale client ID accepted with 202, returns a promptId that will never complete.

After (PR fix — vitest evidence, tmux capture-pane)

npm run dev for full daemon start is blocked by a pre-existing ink/dom package export issue in this environment (Ink 7.0.3 doesn't export ./dom), unrelated to this PR. Using vitest as the after-behavior evidence:

=== BEFORE (installed qwen, stale client ID) ===
POST /session/<id>/prompt with X-Qwen-Client-Id: stale-client-123
Response: 202 Accepted with promptId - BUG: client hangs waiting

=== AFTER (PR fix, unit test evidence) ===

 ✓ src/serve/server.test.ts (517 tests | 516 skipped) 54ms
 Test Files  1 passed (1)
      Tests  1 passed | 516 skipped (517)

--- bridge admission tests ---
 ✓ src/bridge.test.ts (296 tests | 292 skipped) 30ms
 Test Files  1 passed (1)
      Tests  4 passed | 292 skipped (296)

--- SDK client tests ---
 ✓ test/unit/DaemonClient.test.ts (168 tests | 166 skipped) 13ms
 Test Files  1 passed (1)
      Tests  2 passed | 166 skipped (168)

The focused tests verify:

  • Daemon route: stale client ID → 400, response body has code: 'invalid_client_id', no promptId
  • Bridge: sendPrompt throws InvalidClientIdError synchronously (not rejected promise), activePromptCount stays at 0
  • SDK: non-blocking prompts map to DaemonHttpError, blocking prompts release the local slot so a subsequent valid prompt succeeds

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Stepping back to think about this as a whole.

My independent proposal for the fix was identical to what the PR does: remove the try/catch → Promise.reject wrapper around resolveTrustedClientId so the synchronous throw reaches the route handler's existing error mapping. The PR didn't miss a simpler path — this is the simplest path.

Does it solve something users care about? Yes. The before-test curl output clearly shows the bug: a stale client ID gets 202 with a promptId, and the client waits forever. The after-behavior (verified through focused tests) returns 400 invalid_client_id immediately, letting clients clear their pending state.

The code is straightforward — 10 lines of production logic change, no abstractions, no drive-by refactors. The daemonLog.warn addition follows the existing pattern. Test coverage is solid across bridge, daemon route, and SDK.

Every change in the diff is needed for the stated goal. The bridge test updates (rejectstoThrow) are necessary because the behavior changed from async rejection to sync throw. The new tests at each layer verify a different aspect of the fix. Nothing extraneous.

If I had to maintain this in six months, I'd thank the author — the change is minimal, well-tested, and the motivation is clearly documented.

Approving. ✅

中文说明

退后一步整体思考。

我对这个修复的独立提案与 PR 的做法完全一致:去掉 resolveTrustedClientId 外面的 try/catch → Promise.reject 包装,让同步 throw 到达路由处理器已有的错误映射。PR 没有遗漏更简单的路径 —— 这就是最简单的路径。

是否解决了用户关心的问题?是的。before 测试的 curl 输出清楚展示了 bug:stale client ID 得到 202promptId,客户端永远等待。after 行为(通过针对性测试验证)立即返回 400 invalid_client_id,让客户端清理 pending 状态。

代码直截了当 —— 10 行生产逻辑变更,没有抽象,没有顺手重构。daemonLog.warn 添加遵循现有模式。测试覆盖在 bridge、daemon 路由和 SDK 三个层面都很扎实。

diff 中的每个改动都是为了实现声明的目标。bridge 测试更新(rejectstoThrow)是必要的,因为行为从异步拒绝变为同步 throw。每一层的新测试验证修复的不同方面。没有多余内容。

如果六个月后要我维护这段代码,我会感谢作者 —— 改动最小、测试充分、动机文档清晰。

批准 ✅

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

@qwen-code-ci-bot qwen-code-ci-bot added daemon type/bug Something isn't working as expected labels Jun 23, 2026

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

[Suggestion] The synchronous InvalidClientIdError throw from sendPrompt now routes through the catch block in server.ts:3428, which logs PromptQueueFullError at warn level but is silent for InvalidClientIdError. The old (buggy) async path accidentally logged this via the .then() error handler. The new correct path eliminates that log as a side effect — a stale client hammering the daemon with invalid client IDs produces zero daemon-side signal. Consider adding a parallel log:

if (daemonLog && err instanceof InvalidClientIdError) {
  daemonLog.warn('prompt admission rejected: invalid client id', {
    sessionId,
    ...(clientId !== undefined ? { clientId } : {}),
  });
}

[Suggestion] The fakeBridge.sendPrompt in server.test.ts:1087 mirrors the real bridge's non-async admission contract but lacks a comment documenting it. If a future maintainer adds async to this method, all synchronous admission throws silently become rejected promises and the HTTP route returns 202 for errors that should be 400/503 — with a confusing test failure as the only signal. Consider adding a comment mirroring the real bridge's contract at bridge.ts:2805-2806.

— qwen3.7-max via Qwen Code /review

Log invalid prompt client admission rejections and document the fake bridge's synchronous admission contract.

Co-authored-by: Qwen-Coder <[email protected]>
Comment thread packages/acp-bridge/src/bridge.test.ts
Comment thread packages/sdk-typescript/test/unit/DaemonClient.test.ts

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No issues found. Downgraded from Approve to Comment: CI still running.

Clean, well-scoped bug fix. The synchronous-throw contract for sendPrompt is correctly implemented — both call sites (server.ts and dispatch.ts) handle the new InvalidClientIdError throw, the prompt slot accounting is verified by activePromptCount assertions, and the SDK tests cover both blocking and non-blocking prompt paths. LGTM ✅

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

✅ Maintainer local verification — build, 981 unit tests, an independent real-HTTP e2e, and a mutation test

Verified PR #5784 at head 6a3bd53cf in an isolated worktree (fresh npm ci, Node 22.22.2). Beyond re-running the PR's suites, I wrote an independent supertest harness that drives the real createServeApp over real HTTP to demonstrate the bug→fix contract directly. Summary first, evidence below.

Verdict: safe to merge. Clean, focused fix; build + typecheck exit 0; all touched suites pass (981 tests); my independent e2e reproduces the stuck-client bug and confirms the fix; a mutation test proves the change is guarded. CI is already green on macOS/Ubuntu/Windows.

1. Root cause & the fix (one-line behavioral pivot)

The whole fix is making AcpSessionBridge.sendPrompt throw synchronously for an invalid/stale client id instead of returning a rejected promise:

// packages/acp-bridge/src/bridge.ts
-      let originatorClientId: string | undefined;
-      try {
-        originatorClientId = resolveTrustedClientId(entry, context?.clientId);
-      } catch (err) {
-        return Promise.reject(err);   // ← async: HTTP route had already sent 202
-      }
+      const originatorClientId = resolveTrustedClientId(entry, context?.clientId);
//        ↑ throws synchronously, like PromptQueueFullError → route can reject before 202

The /prompt route wraps sendPrompt in a synchronous try/catch; only a synchronous throw is caught before the 202 is written. sendBridgeError already maps InvalidClientIdError → 400 invalid_client_id, and bridgeTypes.ts now documents InvalidClientIdError alongside PromptQueueFullError as a synchronous admission failure. server.ts adds one daemonLog.warn line. The SDK change is test-only (no DaemonClient.ts source change) — it pins the existing DaemonHttpError mapping.

2. Build + typecheck — exit 0

step result
npm run build (all workspaces) BUILD_EXIT=0
npm run typecheck (all workspaces) TYPECHECK_EXIT=0

3. Unit tests — 981 passed across the 3 touched suites

package suite tests
acp-bridge bridge.test.ts ✅ 296
cli server.test.ts ✅ 517
sdk-typescript DaemonClient.test.ts ✅ 168

4. Independent real-HTTP e2e (my own harness, not the PR's) — the bug→fix, demonstrated

A standalone supertest spec driving the real createServeApp with a minimal injected bridge, toggling only the admission behavior:

injected sendPrompt behavior models HTTP result
sync-throw InvalidClientIdError the fix 400 invalid_client_id, no promptId
async-reject InvalidClientIdError the old/buggy bridge 202 + a promptId that never completes (orphaned)
resolves a valid prompt 202 + promptId (no regression)
sync-throw PromptQueueFullError existing contract 503, no promptId

For the same stale client id, the pre-fix async-reject path returns 202 + orphaned promptId (the client waits forever) while the fixed sync-throw path returns 400 (the client recovers immediately). This is exactly the user-visible contract the PR restores, proven end-to-end over real HTTP. 4 passed (4).

5. Mutation test — the change is non-vacuous

Reverted the production files to base (ac3c8ea13), kept the PR's tests:

reverted file suite result guards
bridge.ts bridge.test.ts ❌ 2 failed (detachClient unregisters…, setSessionModel rejects unregistered…) the synchronous toThrow(InvalidClientIdError) admission contract
server.ts server.test.ts 400 without promptId…invalid client admission fails on the daemonLog.warn assertion the new route log

Restoring the PR source → all green again.

Honest note: the full-suite mutation run also showed 2 other red tests (workspace_mismatch…, permission voter invalid_client_id). I re-ran each in isolation against base server.ts and both pass — they are test-ordering/pollution flakes in the reverted full-suite run, not a dependency on this PR. At clean head the suite is a stable 517/517.

Risk notes (low)

  • Behavioral change is narrow and matches the documented contract: a previously async admission failure for invalid client ids is now a synchronous throw. The one caller-facing consequence (direct sendPrompt callers now get a sync throw instead of a rejected promise) is called out in the PR and matches the PromptQueueFullError precedent.
  • No wire-shape change; the daemon reuses the existing invalid_client_id response.

ci-bot is at LGTM, no unresolved review threads, CI green on all three platforms; this independent local run agrees.

中文版(完整对应)

✅ 维护者本地验证 —— 构建、981 个单测、独立真实 HTTP 端到端、变异测试

在 head 6a3bd53cf 的独立 worktree 内全新 npm ci(Node 22.22.2)验证 PR #5784。除重跑 PR 自带套件外,我还写了一个驱动真实 createServeApp over 真实 HTTP(supertest)的独立 harness 来直接演示 bug→fix 契约。先结论,后证据。

结论:可以安全合并。 干净、聚焦的修复;build + typecheck 均 exit 0;所有改动套件通过(981 测试);我的独立 e2e 复现了卡死客户端的 bug 并确认修复有效;变异测试证明改动被守护。CI 三平台已全绿。

1. 根因与修复(一行行为级转折)

整个修复就是让 AcpSessionBridge.sendPrompt 对无效/stale client id 同步抛出,而非返回 rejected promise:

// packages/acp-bridge/src/bridge.ts
-      let originatorClientId: string | undefined;
-      try {
-        originatorClientId = resolveTrustedClientId(entry, context?.clientId);
-      } catch (err) {
-        return Promise.reject(err);   // ← 异步:HTTP 路由此时已经发了 202
-      }
+      const originatorClientId = resolveTrustedClientId(entry, context?.clientId);
//        ↑ 同步抛出,和 PromptQueueFullError 一样 → 路由能在 202 之前拒绝

/prompt 路由用同步 try/catch 包裹 sendPrompt,只有同步抛出才能在写 202 之前被捕获。sendBridgeError 本就把 InvalidClientIdError → 400 invalid_client_id,bridgeTypes.ts 现在把 InvalidClientIdErrorPromptQueueFullError 并列记为同步 admission 失败。server.ts 加了一行 daemonLog.warn。SDK 改动仅测试文件(无 DaemonClient.ts 源改动)—— 它钉住既有的 DaemonHttpError 映射。

2. 构建 + typecheck —— exit 0

步骤 结果
npm run build(所有 workspace) BUILD_EXIT=0
npm run typecheck(所有 workspace) TYPECHECK_EXIT=0

3. 单元测试 —— 3 个改动套件 981 通过

套件 测试数
acp-bridge bridge.test.ts ✅ 296
cli server.test.ts ✅ 517
sdk-typescript DaemonClient.test.ts ✅ 168

4. 独立真实 HTTP 端到端(我自己写的,非 PR 自带)—— bug→fix 实证

一个自包含 supertest 用例,驱动真实 createServeApp,仅切换注入 bridge 的 admission 行为:

注入的 sendPrompt 行为 模拟 HTTP 结果
同步抛 InvalidClientIdError 修复后 400 invalid_client_id, promptId
异步 reject InvalidClientIdError 修复前/有 bug 的 bridge 202 + 永不完成的 promptId(孤儿)
resolve 有效 prompt 202 + promptId(无回归)
同步抛 PromptQueueFullError 既有契约 503,无 promptId

同一 stale client id:修复前异步 reject 路径返回 202 + 孤儿 promptId(客户端永久等待),修复后同步抛出路径返回 400(客户端立即恢复)。这正是本 PR 恢复的用户可见契约,在真实 HTTP 上端到端证明。4 passed (4)

5. 变异测试 —— 改动非空过

把生产文件回退到 base(ac3c8ea13),保留 PR 测试:

回退文件 套件 结果 守护内容
bridge.ts bridge.test.ts ❌ 2 失败(detachClient unregisters…setSessionModel rejects unregistered…) 同步 toThrow(InvalidClientIdError) admission 契约
server.ts server.test.ts 400 without promptId…invalid client admissiondaemonLog.warn 断言处失败 新增的路由日志

恢复 PR 源码 → 重新全绿。

诚实说明: 全套件 mutation 运行时还出现了另外 2 个红(workspace_mismatch…permission voter invalid_client_id)。我把它们各自单独对 base server.ts 重跑,均通过 —— 它们是回退后全套件运行下的测试顺序/污染 flake,与本 PR 无依赖关系。干净 head 下套件稳定 517/517。

风险评估(低)

  • 行为变更范围窄且符合文档契约:无效 client id 此前的异步 admission 失败现改为同步抛出。唯一面向调用方的后果(直接调用 sendPrompt 者现在得到同步抛出而非 rejected promise)已在 PR 中说明,且与 PromptQueueFullError 先例一致。
  • 无 wire-shape 变更;daemon 复用既有的 invalid_client_id 响应。

ci-bot 已 LGTM,无未决 review 线程,CI 三平台全绿;本独立本地运行结论一致。

@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 review findings. Downgraded from Approve to Comment: CI still running.

Clean, well-scoped bug fix. The synchronous-throw contract for sendPrompt is correctly implemented — InvalidClientIdError now propagates before the 202 response, both call sites (server.ts and dispatch.ts) handle the synchronous throw via try/catch, and all 981 tests pass. The new activePromptCount assertions verify no slot leak on rejection. Documentation in bridgeTypes.ts is updated to match.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jun 24, 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.

No review findings. Downgraded from Approve to Comment: CI still running. Clean, well-scoped bug fix. The synchronous-throw contract for sendPrompt is correctly implemented — InvalidClientIdError now propagates before the 202 response, both call sites (server.ts and dispatch.ts) handle the sync throw in their existing try/catch, counter state stays clean (no activePromptCount leak), the daemonLog.warn branch follows the existing PromptQueueFullError pattern, and the SDK's prompt-slot release is verified by the new blocking-prompt test. Build and all targeted tests pass (296 acp-bridge + 517 cli + 168 sdk-typescript).

— qwen3.7-max via Qwen Code /review

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship. ✅

@wenshao
wenshao merged commit 84745d0 into QwenLM:main Jun 24, 2026
48 checks passed
@doudouOUC
doudouOUC deleted the codex/fix-stale-client-prompt-admission branch June 24, 2026 03:05
doudouOUC added a commit that referenced this pull request Jun 24, 2026
* fix(sdk): self-heal stale clientId on invalid_client_id prompts

After a daemon restart or session reload the daemon's in-memory client
registration is wiped, so a prompt sent with our now-unknown clientId is
rejected at admission with 400 invalid_client_id (PR #5784). That rejection
happens before the turn registers, so the prompt never ran and retrying cannot
double-execute.

DaemonSessionClient.prompt() now wraps the admission call (both the blocking and
non-blocking paths) in a self-heal: on invalid_client_id it re-registers via
resumeSession to obtain a fresh clientId and retries the admission exactly once.
A single-flight guard coalesces concurrent prompts so re-registration happens
once; any other error (and a second invalid_client_id) propagates.

Adds 6 tests covering both paths, the retry bound, the non-matching-error guard,
reattach-failure propagation, and concurrent single-flight.

Design: docs/superpowers/specs/2026-06-24-daemon-clientid-self-heal-design.md

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* test(integration): cover real-daemon invalid_client_id contract for self-heal

Adds an integration case to qwen-serve-routes.test.ts validating the three
real-daemon behaviors DaemonSessionClient's clientId self-heal depends on:
(1) an unregistered prompt clientId is rejected at admission with
400 invalid_client_id, (2) resume re-registers and mints a fresh clientId, and
(3) retrying admission with that clientId is accepted (202). Model-free: prompt
admission runs before any model call, so promptNonBlocking returns 202 on
acceptance without reaching the model.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(sdk): Adjust daemon browser bundle budget

Co-authored-by: Qwen-Coder <[email protected]>

* test(sdk): Cover clientId self-heal review cases

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

daemon type/bug Something isn't working as expected

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants