Skip to content

feat(daemon): keep model & approval-mode state consistent across clients sharing a session#4613

Merged
chiga0 merged 16 commits into
daemon_mode_b_mainfrom
feat/daemon-sidechannel-state-layer
Jun 8, 2026
Merged

feat(daemon): keep model & approval-mode state consistent across clients sharing a session#4613
chiga0 merged 16 commits into
daemon_mode_b_mainfrom
feat/daemon-sidechannel-state-layer

Conversation

@chiga0

@chiga0 chiga0 commented May 29, 2026

Copy link
Copy Markdown
Collaborator

Problem

A single daemon session is shared by multiple clients (chat view, terminal view, IDE companion). Two pieces of side-channel state — the current model and the current approval mode — were not reliably kept in sync across those clients:

  1. Broadcasts were duplicated or dropped. Model/approval-mode changes were published from 4 different code paths (direct HTTP call, bridge back-fill, in-session promotion…). Each path published on its own, so the same change could broadcast twice, or a path could forget to broadcast and leave peer clients stale.
  2. State could drift after an async roundtrip. Switch requests round-trip to the agent asynchronously. If the agent's real state ended up different from what the bridge cached (failed switch, agent override), clients were left showing the wrong value with no correction.
  3. In-session mode changes were invisible to peers. When the approval mode changed inside a session (exit_plan_mode, user choosing ProceedAlways), only the legacy current_mode_update notification fired — it was never promoted to a real approval_mode_changed broadcast, so chat and terminal views disagreed.
  4. A fresh attach couldn't see current state. A newly attached (or reconnecting) client only received events going forward; it had no way to learn the current model / approval mode without an extra request.

Solution

  • Single source of truth + atomic publish (§2.3). Cache currentModelId / currentApprovalMode and per-axis generation counters on SessionEntry; route every broadcast through publishModelSwitched / publishApprovalModeChanged so cache update and publish happen together. Replaces the 4 scattered inline publish sites → exactly one broadcast per change.
  • Post-roundtrip reconciliation (§2.2). reconcileAfterRoundtrip runs fire-and-forget in the finally of applyModelServiceId / setSessionModel / setSessionApprovalMode, reads the agent's real state, and emits a corrective broadcast if it differs from cache. Guarded by a per-axis non-recursion lock and a generation TOCTOU check so reconciliation never re-triggers itself or clobbers a newer change.
  • In-session mode promotion (A2). Session.setMode / sendCurrentModeUpdateNotification emit qwen/notify/session/mode-update; the bridge promotes it to approval_mode_changed (still dual-emitting legacy session_update for IDE-companion compat), with an in-flight suppress guard so it doesn't race an explicit switch.
  • Snapshot on attach (A5). GET /session/:id/events?snapshot=1 injects a synthetic session_snapshot frame right after replay_complete, carrying the current model + approval mode so a client can rebuild side-channel state with zero extra round-trips.

Benefit

Every client sharing a session now sees a consistent current model and approval mode: changes broadcast once, drift is self-corrected, in-session changes propagate to all views, and new/reconnecting clients get current state immediately on attach.

Changed files

File Change
packages/acp-bridge/src/bridge.ts State cache fields, publish helpers, reconciliation helper, snapshot wrapper in subscribeEvents
packages/acp-bridge/src/bridgeClient.ts handleInSessionModeUpdate, onModePromoted callback, approvalModeRoundtripInFlight on BridgeClientSessionEntry
packages/acp-bridge/src/bridgeTypes.ts snapshot?: boolean on subscribeEvents opts
packages/cli/src/acp-integration/session/Session.ts extNotification('qwen/notify/session/mode-update') in setMode + sendCurrentModeUpdateNotification
packages/cli/src/serve/server.ts ?snapshot=1 query param parsing
packages/sdk-typescript/src/daemon/events.ts session_snapshot event type, data interface, validator, reducer
packages/sdk-typescript/src/daemon/index.ts Re-export DaemonSessionSnapshotData / DaemonSessionSnapshotEvent

Test plan

  • publishModelSwitched updates cache and publishes model_switched
  • publishApprovalModeChanged publishes approval_mode_changed on setSessionApprovalMode
  • A2: promotes current_mode_update to approval_mode_changed when no bridge roundtrip in flight
  • A2: suppresses current_mode_update while bridge approval-mode roundtrip in flight
  • A2: drops malformed mode-update params without throwing
  • A5: ?snapshot=1 yields session_snapshot after replay_complete
  • A5: no session_snapshot when snapshot is not set
  • SDK: asKnownDaemonEvent narrows session_snapshot
  • SDK: reducer seeds currentModelId and approvalMode from snapshot
  • SDK: reducer does not overwrite model/mode with null snapshot values
  • SDK: drops malformed session_snapshot (missing sessionId)
  • All 215 bridge tests pass (zero regressions)
  • All 84 SDK daemon events tests pass

🤖 Generated with Qwen Code

@github-actions

Copy link
Copy Markdown
Contributor

📋 Review Summary

This PR implements a bridge side-channel state layer with three key features: state cache with atomic publish helpers (§2.3), post-roundtrip reconciliation (§2.2), A2 mode promotion, and A5 session snapshots. The implementation is well-structured with comprehensive test coverage. The code demonstrates strong attention to race condition prevention and backward compatibility.

🔍 General Feedback

  • Strong test coverage: 12 new test cases covering publish helpers, A2 mode promotion (including suppression during bridge roundtrip and malformed params), and A5 session snapshots. Tests verify both positive and negative cases.
  • Good atomic design: Centralizing cache updates + generation bumps + publish in publishModelSwitched and publishApprovalModeChanged prevents TOCTOU issues across multiple call sites.
  • Thoughtful race condition handling: The approvalModeRoundtripInFlight flag mirrors the model roundtrip pattern, and the generation-based TOCTOU guard in reconcileAfterRoundtrip is well-implemented.
  • Backward compatibility: Dual-emit for legacy IDE companion (session_update{current_mode_update}) is a pragmatic transition approach with a clear TODO for removal.
  • Consistent patterns: Model and approval-mode reconciliation share the same structure, reducing cognitive load and making the code easier to maintain.

🎯 Specific Feedback

🟡 High

  • File: packages/acp-bridge/src/bridge.ts:3517-3520 - The reconcileAfterRoundtrip function reads agent state via requestSessionStatus but this function is not defined in the diff. Ensure this helper is properly imported and handles all error cases (e.g., session already shutdown during reconciliation).

  • File: packages/acp-bridge/src/bridgeClient.ts:704 - The dual-emit legacy session_update{current_mode_update} happens unconditionally after the approval_mode_changed publish. If the bus is closed between the two publishes, you'll log the mode promotion but the legacy event is lost silently. Consider wrapping both in a single try-catch or at least logging if the legacy emit fails.

🟢 Medium

  • File: packages/acp-bridge/src/bridge.ts:239-247 - The state cache fields (currentModelId, currentApprovalMode, generation counters) are added to SessionEntry but not initialized in createSessionEntry. While TypeScript allows optional fields, explicitly initializing modelPublishGeneration: 0 and approvalModePublishGeneration: 0 at entry creation (lines 1787-1788 do this) is good — consider adding inline comments linking to §2.3 for consistency with the field comments.

  • File: packages/acp-bridge/src/bridgeClient.ts:670-672 - The type check typeof sessionId !== 'string' || typeof currentModeId !== 'string' silently drops malformed params. This is correct behavior, but consider adding a writeStderrLine log here (like the no-entry case) to aid debugging if malformed notifications appear in production.

  • File: packages/sdk-typescript/src/daemon/events.ts:2413-2416 - The isSessionSnapshotData validator only checks for sessionId being a non-empty string, but allows currentModelId and currentApprovalMode to be any type (including undefined). The reducer handles null values correctly, but consider validating that these fields are either string | null to catch malformed snapshots earlier.

🔵 Low

  • File: packages/acp-bridge/src/bridge.ts:1400 - The comment "Tail-swallow so a failed change doesn't poison subsequent ones" appears multiple times. Consider extracting this pattern into a named helper like withTailSwallow(promise, context) to reduce repetition and make the intent clearer.

  • File: packages/cli/src/acp-integration/session/Session.ts:1697-1706 - The extNotification call in setMode uses void to ignore the promise. This is fine, but consider adding a brief comment explaining why fire-and-forget is acceptable here (e.g., "Non-critical notification — failure doesn't affect mode change correctness").

  • File: packages/acp-bridge/src/bridgeClient.ts:711 - The TODO comment for dual-emit removal is excellent. Consider adding a tracking issue number (e.g., #XXXX) to make it easier to find and clean up later.

  • File: packages/sdk-typescript/src/daemon/events.ts:1690-1692 - The reducer's handling of null snapshot values (...(event.data.currentModelId != null ? { currentModelId: ... } : {})) is correct but subtle. A brief comment explaining "Preserve existing state if snapshot contains null" would help future maintainers understand the intent.

✅ Highlights

  • Excellent race condition prevention: The approvalModeRoundtripInFlight flag staying true through persist + publish phase shows deep understanding of async timing issues.

  • Clean abstraction boundaries: The publish helpers (publishModelSwitched, publishApprovalModeChanged) are private to the bridge module, preventing external misuse while keeping call sites simple.

  • Robust error handling: Malformed mode-update params are dropped without throwing, and the iterator continues to work correctly — verified by test coverage.

  • Thoughtful snapshot design: The withSnapshot wrapper yields the synthetic session_snapshot frame immediately after replay_complete, capturing state synchronously at yield time. This avoids an extra round-trip for reconnecting clients.

  • Comprehensive test matrix: Tests cover cache updates, generation bumps, A2 suppression during bridge roundtrip, dual-emit for legacy compat, and snapshot reducer behavior with null values.

@chiga0 chiga0 changed the title feat(daemon): bridge side-channel state layer — A1 follow-up + A2 + A5 feat(daemon): keep model & approval-mode state consistent across clients sharing a session May 29, 2026
@chiga0 chiga0 requested review from doudouOUC and wenshao May 29, 2026 03:46

@chiga0 chiga0 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

LGTM. Clean solution to the distributed state consistency problem.

Architecture highlights:

  • §2.3 centralized publish helpers with atomic cache + generation counter — eliminates the 4 scattered publish sites cleanly
  • §2.2 reconcileAfterRoundtrip with generation TOCTOU guard is a solid pattern — prevents clobbering newer changes during reconciliation
  • A2 suppress guard (approvalModeRoundtripInFlight) correctly prevents race between bridge-driven and agent-initiated mode changes
  • A5 withSnapshot generator injects session_snapshot after replay_complete with zero extra round-trips

Test coverage: 11 new tests (7 bridge + 4 SDK) covering publish helpers, promotion, suppression, malformed input, snapshot on/off, reducer seeding, and null preservation.

Nit (non-blocking): handleInSessionModeUpdate fallback path (bridgeClient.ts:686) publishes approval_mode_changed with only { sessionId, next } — missing previous and persisted fields compared to the main path. Since the onModePromoted callback is always provided by the bridge factory in production, this is a theoretical inconsistency only. Consider adding previous: entry.currentApprovalMode ?? 'default', persisted: false for consistency.

This review was generated by QoderWork AI

Comment thread packages/acp-bridge/src/bridge.ts Outdated
Comment thread packages/acp-bridge/src/bridge.ts Outdated
Comment thread packages/acp-bridge/src/bridgeClient.ts Outdated
Comment thread packages/cli/src/acp-integration/session/Session.ts
Comment thread packages/acp-bridge/src/bridge.ts
Comment thread packages/acp-bridge/src/bridge.ts
Comment thread packages/cli/src/acp-integration/session/Session.ts
Comment thread packages/acp-bridge/src/bridge.ts Outdated
Comment thread packages/sdk-typescript/src/daemon/events.ts
Comment thread packages/acp-bridge/src/bridgeClient.ts Outdated
Comment thread packages/acp-bridge/src/bridge.ts Outdated
Comment thread packages/acp-bridge/src/bridge.ts Outdated
Comment thread packages/acp-bridge/src/bridge.ts
Comment thread packages/acp-bridge/src/bridge.ts Outdated
Comment thread packages/acp-bridge/src/bridgeClient.ts
Comment thread packages/acp-bridge/src/bridge.test.ts
@wenshao

wenshao commented May 29, 2026

Copy link
Copy Markdown
Collaborator

🔬 Local Verification Report

Verified by: @wenshao
Branch: feat/daemon-sidechannel-state-layer
Head commit: 3bd1224 (fix(daemon): address review on side-channel state consistency)
Environment: macOS Darwin 25.4.0 / Node.js v22.17.0 / vitest 3.2.4


TypeScript Compilation

Package Result
packages/acp-bridge ✅ Clean (0 errors)
packages/cli ✅ Clean (0 errors)
packages/sdk-typescript ✅ Clean (0 errors)

Test Results

Test Suite Files Tests Result Duration
@qwen-code/acp-bridge (full package) 7 passed 342 passed ✅ All pass 9.12s
@qwen-code/sdk daemon events 1 passed 86 passed ✅ All pass 1.00s
Total 8 passed 428 passed

New Test Cases Verification (PR-specific)

Bridge — side-channel state layer (#4511):

Test Status
publishModelSwitched updates cache and publishes model_switched
publishApprovalModeChanged publishes approval_mode_changed on setSessionApprovalMode
A2: promotes current_mode_update to approval_mode_changed when no bridge roundtrip in flight
A2: suppresses current_mode_update while bridge approval-mode roundtrip in flight
A2: drops malformed mode-update params without throwing
A5: yields session_snapshot after replay_complete when snapshot=true
A5: does NOT yield session_snapshot when snapshot is not set
A5: yields session_snapshot up front on a fresh connection (no Last-Event-ID)
§2.2: publishes corrective model_switched when agent state drifted from cache
§2.2: does NOT publish corrective event when agent state matches cache
§2.2: swallows failed status read without crashing or masking original change
§2.2: does NOT reconcile when model roundtrip itself fails

SDK — session_snapshot (A5 #4511):

Test Status
asKnownDaemonEvent narrows session_snapshot
reducer seeds currentModelId and approvalMode from snapshot
reducer does not overwrite model/mode with null snapshot values
drops malformed session_snapshot (missing sessionId)
drops session_snapshot with non-string currentModelId
drops session_snapshot with non-string currentApprovalMode

Regression Check

  • ✅ All 342 existing bridge tests pass (zero regressions)
  • ✅ All 86 SDK daemon events tests pass (zero regressions)
  • ✅ No type errors introduced in any of the 3 changed packages

Notes

  • The full project npm run build has a pre-existing dependency issue (ws package missing for @google/genai) unrelated to this PR — this does not affect the packages changed by this PR.
  • All PR-specific test cases covering publish helpers, A2 mode promotion/suppression, A5 session snapshot injection, and §2.2 reconciliation (drift/match/failure/roundtrip-failure) verified locally.

Verdict

Ready to merge. All 428 tests pass, no type errors, no regressions. The implementation is consistent with the design doc and test plan.

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

Follow-up review of the latest revision. One finding (the applyModelServiceId reconcile guard) is a still-open tail of the resolved bridge.ts:1405 thread — the succeeded gate was added to two of the three reconcile sites but not this one. Two further suggestions below. Lower-confidence items (cold-attach snapshot seeding, workspace-mirror replay on reconcile) are left out as they may be intended scope.

Comment thread packages/acp-bridge/src/bridge.ts Outdated
Comment thread packages/cli/src/acp-integration/session/Session.ts
Comment thread packages/acp-bridge/src/bridge.ts
@wenshao

wenshao commented May 29, 2026

Copy link
Copy Markdown
Collaborator

✅ Local verification report — PR #4613

Built and tested locally in an isolated git worktree (tmux session), against the merged state (PR + current base), which is what matters for the merge decision.

Environment

PR head 3bd122473 (fix(daemon): address review on side-channel state consistency)
Base daemon_mode_b_main 117c9b2cf
Tested commit (PR ⨉ base merge) 66ff06414merge is clean, no conflicts
Node / npm v22.22.2 / 10.9.7

Build & typecheck — all clean

Step Result
build @qwen-code/qwen-code-core ✅ exit 0
typecheck @qwen-code/acp-bridge (tsc --noEmit) ✅ exit 0
typecheck @qwen-code/sdk (tsc --noEmit) ✅ exit 0

Test suites — all green, zero regressions

Suite Tests Result
@qwen-code/acp-bridge (full) 343 passed (7 files)
@qwen-code/sdk (full) 705 passed (15 files)
@qwen-code/qwen-codeserve/server.test.ts 339 passed
@qwen-code/qwen-codeacp-integration/session/Session.test.ts 81 passed

PR-specific tests confirmed executing & passing

Ran the new describe-blocks in isolation (vitest -t) to confirm they are real and exercised, not silently skipped:

bridge.test.ts — 12/12 pass (createHttpAcpBridge — side-channel state layer (#4511))

  • §2.3 publishModelSwitched / publishApprovalModeChanged update cache + publish exactly once
  • A2 promotion of current_mode_updateapproval_mode_changed; suppression while a bridge roundtrip is in flight; malformed params dropped without throwing
  • A5 session_snapshot after replay_complete (with snapshot=1), up-front on a fresh connection (no Last-Event-ID), and not emitted when unset
  • §2.2 reconciliation: corrective model_switched on drift; no event when state matches; failed status read swallowed; no reconcile when the roundtrip itself failed

daemonEvents.test.ts — 6/6 pass (session_snapshot (A5 #4511))

  • asKnownDaemonEvent narrows the type; reducer seeds currentModelId/approvalMode; null values don't overwrite existing state; malformed (missing sessionId, non-string currentModelId/currentApprovalMode) is dropped and counted

The runtime [reconcile] … action=corrected/failed and [demux] … action=promoted/suppressed/dropped operator logs were observed in the live test output, matching the documented behavior.

Notes / suggestions for follow-up (none blocking)

  1. CLI-side Session.ts coverage gap. The PR adds two new behaviors to Session.setMode / sendCurrentModeUpdateNotification — (a) throwing RequestError.invalidParams on an unknown modeId, and (b) emitting the qwen/notify/session/mode-update extNotification. Neither is asserted in Session.test.ts (existing setMode tests only cover valid-mode mapping). The bridge tests cover the consumption/promotion of that notification, but not its emission from the Session. A small CLI-side test would close the loop — mirrors the existing setModel "emits a current_model_update extNotification (A1)" test at Session.test.ts:488.
  2. server.ts ?snapshot=1 parsing has no dedicated server-layer test; it's covered transitively by the bridge subscribeEvents tests. Trivial 2-line change, low risk.
  3. A5 has no real client consumer yet. ?snapshot=1 and the session_snapshot SDK event are a foundation layer; no client in this PR actually requests the snapshot. This is consistent with the PR being a state layer — end-to-end exercise will come with the *-daemon-*-wireup consumer PRs. Flagging so the wireup PRs are tracked.
  4. Lockfile drift (not caused by this PR). npm ci fails on the merged state because the base branch introduced a new @qwen-code/web-shell workspace not yet reflected in package-lock.json; npm install resolves it. This is a base-branch artifact — PR feat(daemon): keep model & approval-mode state consistent across clients sharing a session #4613 touches no package.json/lockfile.

Verdict

The change builds, typechecks, and passes all relevant suites cleanly on the merged state, with focused tests covering every new code path on the bridge + SDK side. Code is well-reasoned with thorough inline rationale (TOCTOU guard, non-recursion lock, fresh-attach vs resume snapshot placement, malformed-input validation). Recommend merge, optionally after adding the small CLI-side Session.setMode test in note (1).

Verified locally via tmux on the merged worktree; full suites (not just the new tests) were run to check for regressions.

@wenshao

wenshao commented May 29, 2026

Copy link
Copy Markdown
Collaborator

✅ Follow-up: closed the two test-coverage gaps from the prior report

Wrote and ran 4 new tests locally (merged worktree, tmux) that close findings (1) and (2) from my previous comment, then proved they're non-vacuous with a negative control.

New tests — all pass (full files: 424 passed, was 420)

packages/cli/src/serve/server.test.ts — real HTTP E2E of the A5 ?snapshot=1 path (boots runQwenServe, fetches the SSE endpoint, inspects the opts forwarded to the bridge):

  • forwards snapshot:true to subscribeEvents when ?snapshot=1 is set (A5 #4511)
  • does NOT forward snapshot when the query param is absent (A5 #4511)

packages/cli/src/acp-integration/session/Session.test.ts — the in-session mode-switch behaviors the PR added (mirrors the existing setModel A1 test):

  • emits a mode-update extNotification after switching (A2)
  • rejects an unknown modeId and does not change approval mode

Negative control (reverse-audit — passing tests ≠ correct tests)

Reverted only server.ts + Session.ts to the base branch (daemon_mode_b_main), keeping the new tests, and re-ran:

× setMode > emits a mode-update extNotification after switching (A2)
   → expected "spy" to be called with arguments
× setMode > rejects an unknown modeId and does not change approval mode
   → promise resolved "undefined" instead of rejecting
× events > forwards snapshot:true when ?snapshot=1 is set
   → expected undefined to be true
✓ events > does NOT forward snapshot when the query param is absent   (correctly green on both — it asserts absence)
Tests  3 failed | 1 passed

3/4 fail without the PR's source → the tests genuinely pin the new behavior, not a tautology. The 4th is an absence-assertion that is correct on both branches by design. Restored the PR source → back to 4/4 green.

Net

Combined with the prior run (acp-bridge 343, sdk 705, server 339, Session 81 — all green; typechecks clean; clean merge), the daemon side-channel state layer is now covered end-to-end on all three layers — SDK reducer, bridge wrapper/publish/reconcile, and the CLI server + Session emission boundary. Findings (1) and (2) are addressed; (3) (no real client consumer of A5 yet) and (4) (base-branch lockfile drift) remain informational. Merge recommendation stands.

The 4 new tests are small and follow existing patterns in each file — happy to open them as a PR against the branch if useful, or they can be folded into #4613.

Comment thread packages/acp-bridge/src/bridge.ts Outdated
Comment thread packages/acp-bridge/src/bridgeClient.ts
Comment thread packages/cli/src/acp-integration/session/Session.ts
Comment thread packages/acp-bridge/src/bridgeClient.ts Outdated
Comment thread packages/acp-bridge/src/bridge.test.ts
Comment thread packages/sdk-typescript/src/daemon/events.ts
Comment thread packages/acp-bridge/src/bridge.ts
Comment thread packages/acp-bridge/src/bridge.ts Outdated

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

Code Review

中优先级问题(建议修复)

applyModelServiceId 在 roundtrip 失败时仍无条件触发 reconcile,与设计目标不一致

applyModelServiceId(attach 时带 modelServiceId 的路径)的 finally 块中,reconcileAfterRoundtrip 是无条件调用的:

} finally {
  entry.modelRoundtripInFlight = false;
  void reconcileAfterRoundtrip(entry, 'model');
}

而在 HTTP setSessionModel 路径中,你专门引入了 let succeeded = false 标记,只在成功后才触发 reconcile。PR 的测试和注释都明确了设计意图:roundtrip 失败时不应 reconcile,以避免在 model_switch_failed 旁边再出现一条"纠正性 model_switched"

applyModelServiceId 与这一目标不一致。当代理拒绝模型切换时,客户端可能先收到 model_switch_failed,随后又收到 reconcile 补发的 model_switched,语义混乱。

建议: 仿照 setSessionModel 的模式加入 succeeded 守卫,并补充一条对应的回归测试。


低优先级建议

BridgeClient 构造参数注释已过时

现有注释(如 bridgeClient.test.ts 中)写着 "the constructor takes 7 positional args",但本 PR 新增了 onModelPromotedonModePromoted 两个可选参数。建议更新为描述"尾部为可选注入回调"的语义性注释,避免硬编码参数数量。


设计亮点

  • 状态缓存 + 代次计数器 + 非递归 guard 整体自洽,无递归爆炸或死锁风险
  • model / approvalMode 两轴完全独立,互不干扰
  • snapshot 注入对新连接(先 yield snapshot)和恢复连接(replay_complete 后注入)两种场景都正确处理
  • SDK isSessionSnapshotData 严格校验 string | null,防止非字符串写入 state
  • dual-emit legacy session_update{current_mode_update} 保障 IDE companion 向后兼容
  • 测试覆盖了 publish、suppress、malformed 丢弃、reconcile drift/match/failure、snapshot fresh/resume 等关键路径

applyModelServiceId 的 reconcile 触发条件需对齐外,整体实现质量很高。

Comment thread packages/acp-bridge/src/bridge.ts Outdated
Comment thread packages/cli/src/acp-integration/session/Session.ts
Comment thread packages/acp-bridge/src/bridge.test.ts
Comment thread packages/acp-bridge/src/bridgeClient.ts Outdated
Comment thread packages/cli/src/acp-integration/session/Session.ts
wenshao
wenshao previously approved these changes May 30, 2026
wenshao
wenshao previously approved these changes May 30, 2026
Comment thread packages/acp-bridge/src/bridge.ts

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

复核结论(side-channel state consistency #4511

前 3 轮 review 的 critical 项已逐条核对,当前 revision befb556d 基本都已修复:

  • snapshot 对全新 SSE 连接前置注入(lastEventId===undefined
  • reconcile 的 rerun 重触发,连续变更不再丢 reconcile
  • fallback publish 补全 previous/persisted
  • modeMap 未知 id 改为 throw invalidParams
  • corrective publish 加结构化 stderr 日志
  • reconciliation_failed 改 stderr(不再发 bus 事件)
  • SDK snapshot validator 校验 model/mode 的 string | null
  • 上轮提的 applyModelServiceId 失败仍 reconcile —— 现在三个 reconcile 点都有 succeeded 门控

并发模型(generation 计数 + 非递归 flag + rerun 收敛 + suppress guard)也验证过,逻辑自洽:flag 不泄漏卡死、corrective publish 不自触发死循环、drift 不被永久遗漏。👍

本轮新发现 6 条(见行内评论)。其中 #1(A5 冷 attach snapshot)建议合并前处理,会架空该功能的主用例。

另两个 minor 放这里:

  • bridge.ts:1853-1855:reconcile catch 注释称 stale 状态「已由 reconnect 的 state_resync_required 覆盖」,但该事件仅在 reconnect(ring 淘汰 / epoch 重置)时触发,长连接不断线的 client 收不到 —— 代码 OK,注释需修正。
  • bridgeClient.test.ts:20,23:注释仍写「the 7-arg constructor / 7 positional args」,但本 PR 新增了 onModelPromoted / onModePromoted,构造器现为 9 个位置参数,注释已过时。

Comment thread packages/acp-bridge/src/bridge.ts Outdated
Comment thread packages/acp-bridge/src/bridge.ts
Comment thread packages/cli/src/acp-integration/session/Session.ts Outdated
Comment thread packages/acp-bridge/src/bridge.ts
Comment thread packages/acp-bridge/src/bridge.ts
Comment thread packages/acp-bridge/src/bridge.ts Outdated
@chiga0 chiga0 requested a review from doudouOUC May 31, 2026 03:31
chiga0 pushed a commit that referenced this pull request May 31, 2026
…nc, contract cleanup

- bridge: seed snapshot caches (currentModelId/currentApprovalMode) from
  newSession/loadSession responses so a cold attach reports real state
  instead of null/null, with KNOWN_APPROVAL_MODES enum backstop
- bridge: enum-validate the reconcile approvalMode branch and drop unknown
  modes with a logged reason
- bridge: on a persisted approval-mode change, mirror the new workspace
  default into every peer SessionEntry cache so their GET status /
  session_snapshot stop reporting the pre-change mode
- bridge/bridgeClient: remove try/catch wrappers around EventBus.publish()
  per its documented never-throws contract; drop misleading "bus closed"
  comments
- cli/Session: log dropped advisory extNotifications via debugLogger.debug
  instead of swallowing silently
- bridge.test: add failure-gating coverage for applyModelServiceId — a
  rejected attach-time model apply must not trigger reconcile (no status read)

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@chiga0 chiga0 requested a review from wenshao May 31, 2026 07:28
Comment thread packages/acp-bridge/src/bridge.ts

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

[Critical] [test] Session 测试失败:Session.test.ts:2574

resets AUTO denial counters when a permission-request hook approves a denialTracking fallback prompt — spy setAutoModeDenialState 被调用 0 次。PR 在 Session.ts 中删除了 isDenialFallbackReason 导入和相关回调逻辑(含 setAutoModeDenialState(after) 调用),但未同步移除或更新对应测试用例。测试结果:93 passed / 1 failed。

(Posting as body-level comment because the failing test line is not in the PR diff.)

— qwen3.7-max via Qwen Code /review

Comment thread packages/acp-bridge/src/bridge.ts
Comment thread packages/acp-bridge/src/bridge.ts Outdated
Comment thread packages/acp-bridge/src/bridge.ts
Comment thread packages/acp-bridge/src/bridge.ts

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

[Critical] [test] Session.test.ts:2574 — denial-counter reset test fails (93 passed, 1 failed)

The test resets AUTO denial counters when a permission-request hook approves a denialTracking fallback prompt asserts setAutoModeDenialState was called, but the spy is invoked 0 times. The PR's changes to Session.ts (adding setMode validation and extNotification) appear to have altered the prompt flow such that the denial-tracking fallback path is no longer exercised by this test scenario. The test needs to be updated to match the new code behavior, or the code change needs investigation.

(Posted as a body-level comment because the failing test line is not in this PR's diff.)

— qwen3.7-max via Qwen Code /review

Comment thread packages/sdk-typescript/src/daemon/events.ts
Comment thread packages/acp-bridge/src/bridgeTypes.ts
Comment thread packages/acp-bridge/src/bridge.ts Outdated
Comment thread packages/acp-bridge/src/bridge.ts Outdated
Comment thread packages/acp-bridge/src/bridge.ts
@chiga0 chiga0 force-pushed the feat/daemon-sidechannel-state-layer branch from 1ae2bdf to 816bf3d Compare June 8, 2026 02:25

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

[Critical] Session.test.ts:2574 — denial counter reset test still fails (93 passed / 1 failed):

FAIL  Session > prompt > resets AUTO denial counters when a permission-request hook approves a denialTracking fallback prompt
AssertionError: expected "spy" to be called with arguments: [ { consecutiveBlock: +0, …(3) } ]

(Line 2574 is not in this PR's diff, so this comment is posted here instead of inline.)


Needs Human Review (low confidence):

  • bridge.ts:3474setSessionApprovalMode unknown-mode path: succeeded=true skips publish but the returned HTTP response claims the mode was set to the requested value. Reconcile fires but also drops the unknown mode, leaving a temporary three-way divergence between caller, cache, and agent. Self-healing when the agent returns to a known mode, but silent divergence persists while the agent stays in the unknown mode.
  • events.ts:1762session_snapshot reducer uses != null guard: when snapshot sends null for currentModelId/currentApprovalMode, the reducer preserves whatever the previous state had rather than clearing it. In practice null only occurs when the cache was never populated, so the base state typically has nothing stale to preserve.

— qwen3.7-max via Qwen Code /review

Comment thread packages/sdk-typescript/src/daemon/events.ts
Comment thread packages/acp-bridge/src/bridge.ts
Comment thread packages/acp-bridge/src/bridge.ts
Comment thread packages/acp-bridge/src/bridge.ts
Comment thread packages/sdk-typescript/src/daemon/events.ts

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

[Critical] Session.test.ts:2574 — denial-counter reset test fails (93 passed / 1 failed).

FAIL  Session > resets AUTO denial counters when a permission-request hook approves a denialTracking fallback prompt
AssertionError: expected "spy" to be called with arguments: [ { consecutiveBlock: +0, …(3) } ]

setAutoModeDenialState is never called. This test was also failing in R16.


[Critical] daemonEvents.test.ts — SDK test suite cannot run: the events.ts syntax errors (see inline comment) cause esbuild transform failure, blocking all 0 tests from collecting.

— qwen3.7-max via Qwen Code /review

Comment thread packages/sdk-typescript/src/daemon/events.ts
Comment thread packages/acp-bridge/src/bridge.test.ts

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

[Critical] Session.test.ts:2574 — Denial-counter reset test fails (93/1 passed, 1 failed).

Test: "resets AUTO denial counters when a permission-request hook approves a denialTracking fallback prompt"
Error: setAutoModeDenialState spy was never called (0 invocations).

Root cause: Session.ts:2406 (hook-approval path) calls onConfirm(ProceedOnce) but does NOT call this.config.setAutoModeDenialState(recordFallbackApprove(...)). That reset exists only in the requestPermission response path (line 2488-2489). When AUTO mode's denial tracking forces a fallback prompt and a permission-request hook approves it, the denial streak is never reset — permanently downgrading the session to manual approval until the mode is toggled.

Fix: add this.config.setAutoModeDenialState(recordFallbackApprove(this.config.getAutoModeDenialState())) before or after onConfirm in the hook-approval branch at line 2406.

(Posted as a body-level comment because the error is on a line not touched by this PR's diff.)


Deterministic analysis note: 29 tsc errors exist in server.ts and Session.ts but all are on lines NOT in this PR's diff — pre-existing issues on the daemon_mode_b_main base branch.

— qwen3.7-max via Qwen Code /review

Comment thread packages/acp-bridge/src/bridge.ts Outdated
秦奇 and others added 15 commits June 8, 2026 17:26
- inject session_snapshot up front on fresh SSE connections (not only on resume)
- reconcile only after a roundtrip that landed; guard generation TOCTOU with
  one bounded re-run and log skip/correct/fail transitions
- drop unencodable reconciliation_failed bus event in favor of operator log
  (client path already covered by state_resync_required)
- bridgeClient mode fallback emits previous/persisted; dual-emit session_update
  uses the canonical nested data.update shape
- validate modeId at the setMode boundary; reject unknown modes
- SDK session_snapshot validator type-checks currentModelId/currentApprovalMode
- tests: fresh-connection snapshot + reconciliation drift/match/failure/roundtrip-failure
- applyModelServiceId: gate reconcile behind a `succeeded` flag so a
  rejected create/attach-time roundtrip can't pair a corrective
  model_switched with the model_switch_failed it just published; mirrors
  setSessionModel / setSessionApprovalMode.
- in-session mode demux: validate currentModeId against the known
  approval-mode enum (lockstep with Session.setMode) before it fans out
  to SSE clients / the SDK reducer.
- in-session mode demux: suppress the legacy session_update dual-emit on
  the exit_plan_mode path via a `legacyFrameSent` flag — sendUpdate
  already published that frame, so dual-emitting delivered it twice. The
  setMode path (no sendUpdate) keeps its dual-emit.
- reconcile: emit a `reason=roundtrip_failed` skip log on all three
  failure paths so the skipped reconcile is greppable.
- SDK: add session_snapshot to RESYNC_PASSTHROUGH_TYPES so a client that
  reconnects past ring eviction recovers currentModelId / approvalMode
  from the full-state frame instead of staying stale until loadSession.
- tests: approvalMode reconcile drift + roundtrip-fail, generation rerun,
  unknown-mode enum drop, dual-emit shape + suppression, setMode
  extNotification + unknown-modeId rejection.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
The existing A5 snapshot tests only seed currentModelId, leaving the
publishApprovalModeChanged -> entry.currentApprovalMode -> snapshot
pipeline uncovered at the bridge level. Add a test that promotes an
in-session mode change before subscribing and asserts the snapshot
carries the non-null currentApprovalMode.

Generated with AI

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

- bridgeClient.ts: the A2 comment claimed handleInSessionModeUpdate
  "mirrors handleInSessionModelUpdate exactly", but it diverges with enum
  validation and the legacy dual-emit. Reword to state the shared
  suppression pattern plus the two additions.
- Session.test.ts: add coverage for sendCurrentModeUpdateNotification
  asserting the extNotification carries legacyFrameSent: true, so a
  regression dropping it (double legacy frame to the IDE companion) is
  caught.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…nc, contract cleanup

- bridge: seed snapshot caches (currentModelId/currentApprovalMode) from
  newSession/loadSession responses so a cold attach reports real state
  instead of null/null, with KNOWN_APPROVAL_MODES enum backstop
- bridge: enum-validate the reconcile approvalMode branch and drop unknown
  modes with a logged reason
- bridge: on a persisted approval-mode change, mirror the new workspace
  default into every peer SessionEntry cache so their GET status /
  session_snapshot stop reporting the pre-change mode
- bridge/bridgeClient: remove try/catch wrappers around EventBus.publish()
  per its documented never-throws contract; drop misleading "bus closed"
  comments
- cli/Session: log dropped advisory extNotifications via debugLogger.debug
  instead of swallowing silently
- bridge.test: add failure-gating coverage for applyModelServiceId — a
  rejected attach-time model apply must not trigger reconcile (no status read)

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…d cache doc

- bridge: document that setSessionModel caches the raw model id and
  relies on the immediately-following reconcileAfterRoundtrip to
  correct any raw-vs-canonical drift (the bridge layer lacks access
  to the CLI's formatAcpModelId which requires authType)
- bridge: fix stale reconcile-catch comment that referenced
  state_resync_required (long-lived SSE connections don't reconnect)
- bridgeClient.test: update stale "7-arg constructor" comment to
  reflect the current 8-arg constructor

Generated with AI

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

- sdk: bump MAX_DAEMON_BROWSER_BUNDLE_BYTES from 100 KiB to 105 KiB to
  accommodate session_snapshot type/validator/reducer additions (+1.2 KiB)
- bridge: remove redundant entry! non-null assertions (already narrowed
  by if-guard at line 2708)
- bridge: document setSessionModel raw-id cache + reconcile correction
- bridge.test: add seedSnapshotCaches cold-attach test (newSession
  response seeds model+mode without intermediate notifications)
- bridge.test: add peer cache sync test (persisted mode change updates
  peer snapshot)
- bridge.test: add unknown-mode-drop test (reconcile drops agent-
  returned modes not in KNOWN_APPROVAL_MODES)

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…test, add resync passthrough test

- bridge.test: rewrite unknown-mode-drop test to trigger approvalMode
  reconcile (via setSessionApprovalMode) instead of model reconcile
  (via modelServiceId), which never entered the approvalMode branch
  — the original was a false positive (F8E2h)
- bridge.test: fix misleading params.mode cast in peer-cache-sync test;
  status RPC sends {sessionId} not {mode} — return fixed 'yolo' (F8E2o)
- sdk daemonEvents.test: add session_snapshot passthrough-during-resync
  test (RESYNC_PASSTHROUGH_TYPES membership regression guard) (F8SOq)

Generated with AI

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

Add statusReads counter to the unknown-mode-drop test so it positively
asserts that reconcile actually executed (status RPC was called), not
just that no corrective event appeared. Without this, a future refactor
disabling reconcileAfterRoundtrip would make the test pass vacuously.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…test, add resync passthrough test

- bridge.test: restore missing closing braces for extractErrorCode
  describe/it blocks (lost during rebase conflict resolution)
- sdk build.js: bump MAX_DAEMON_BROWSER_BUNDLE_BYTES from 106 to
  107 KiB (actual bundle is 108595 bytes = ~106.1 KiB)

Generated with AI

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

- bridge: validate setSessionApprovalMode extMethod response against
  KNOWN_APPROVAL_MODES before publishing/broadcasting; drop with log
  if agent returns unknown mode (closes trust-boundary gap where
  handleInSessionModeUpdate and reconcile had guards but this path
  did not)
- bridge: add typeof === 'string' guard to model reconcile branch
  so a non-string agent response (e.g. number) cannot pollute the
  cache and break downstream session_snapshot validation
- bridge: add writeStderrLine to seedSnapshotCaches drop branches
  for operator observability parity with reconcile's drop logging

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
- bridge: leave succeeded=false when agent returns unknown approval
  mode — skips pointless reconcile that would re-drop the same value
- bridge: restore channel-overlap HAZARD comment on closeSession's
  channelInfoForEntry call (lost during reaper code removal)

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Three sites where session_snapshot was inserted immediately after
session_rewound lost the preceding block's closing delimiter during
rebase conflict resolution: type alias (missing >;), asKnownDaemonEvent
case (missing : undefined;), and reducer case (missing };).

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…base artifacts)

- bridge: remove session-reaper code (closeSessionImpl, startSession-
  Reaper, stopSessionReaper, constants) inadvertently included during
  rebase conflict resolution — not part of this PR's scope
- events.ts: restore 2 missing delimiters (isSessionBranchedData
  closing brace, session_rewound type/case closers) lost when
  session_snapshot was inserted adjacent to session_branched blocks

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@chiga0 chiga0 force-pushed the feat/daemon-sidechannel-state-layer branch from db57e1f to af76d3b Compare June 8, 2026 09:43
… silent success

When the agent returns a mode not in KNOWN_APPROVAL_MODES, throw
instead of returning a misleading success response. The previous
behavior sent 200 OK echoing the requested mode while the cache
and SSE bus still showed the old value — a three-way state divergence.

Generated with AI

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

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

R19: No high-confidence issues found after full multi-dimensional review (9 agents + reverse audit). The publish helpers, reconciliation logic, snapshot injection, and SDK reducer are all sound. Tests: 247/247 bridge, 87/87 SDK pass. One pre-existing test failure in Session.test.ts (denial-counter reset on hook-approved path) is not introduced by this PR — verified present on base branch. — qwen3.7-max via Qwen Code /review

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

Review Summary

整体实现质量很高,并发设计精巧(generation counter + in-flight guard + bounded rerun)。核心逻辑正确,无 critical 问题。以下 inline comments 为建议改进项,不 block 合并。

架构亮点:

  • §2.3 publish helpers 原子化了 cache + generation + bus publish,消除了原先 4 处散布的 publish site
  • §2.2 reconcile 的 rerun 标志在 corrective publish 之前捕获,不会自触发循环
  • A5 snapshot 对 fresh connection(先 yield)和 reconnect(replay_complete 后注入)两种场景都正确处理
  • EventBus.publish never-throws 合约经代码验证属实,移除 try/catch 包装是正确的简化
  • legacy dual-emit 的 legacyFrameSent flag 优雅解决了 exit_plan_mode 路径的重复发送

测试覆盖: 新增 ~1580 行测试,覆盖了 publish、suppress、malformed drop、reconcile drift/match/failure/roundtrip-fail/generation-rerun、snapshot fresh/resume、SDK reducer seeding/null-preservation 等关键路径。

级别 数量
Critical 0
Important (inline) 6
Suggestion 4

Suggestions (non-inline):

  1. seedSnapshotCaches 的 3 条防御分支(non-string model、empty model、unknown mode)只测了 happy path,建议补测
  2. sendCurrentModeUpdateNotification 只测了 ProceedAlways 分支,RestorePrevious(读 config)和 ProceedOnce 未覆盖
  3. reconcileAfterRoundtrip catch block 对所有异常统一写 [reconcile] ... action=failed,建议对非预期编程错误打不同 tag 方便告警
  4. bridge.test.ts 新增 ~1389 行中测试 boilerplate(ChannelFactory 创建)重复度较高,考虑提取更多共享 helper

const sessionId = params['sessionId'];
const currentModeId = params['currentModeId'];
if (typeof sessionId !== 'string' || typeof currentModeId !== 'string') {
return;

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.

[Important] type guard 静默 return 无日志,与同方法内其他 drop 路径(unknown_mode、no_entry、bridge_roundtrip_in_flight)不一致,也与 prompt-suggestion handler 的 malformed 路径不一致。

建议加 writeStderrLine 保持可观测性:

if (typeof sessionId !== 'string' || typeof currentModeId !== 'string') {
  writeStderrLine(
    `[demux] session=${typeof sessionId === 'string' ? sessionId : '<missing>'} type=current_mode_update action=dropped reason=malformed`,
  );
  return;
}

handleInSessionModelUpdate 有同样的 gap(pre-existing),可一并修复。

// outside this set is dropped before it fans out to SSE clients / the SDK
// reducer. Keep the two in lockstep. Exported so the bridge's reconcile and
// snapshot-seed paths apply the same enum backstop to agent-supplied mode ids.
export const KNOWN_APPROVAL_MODES: ReadonlySet<string> = new Set([

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.

[Important] 此 Set 与 Session.tsmodeMap keys 需手动保持同步,当前值完全一致(plan, default, auto-edit, auto, yolo),但没有编译时保障。注释中的 "Keep the two in lockstep" 容易被忽略。

建议考虑:

  1. 将 mode string literals 提取为共享 const(放在 @qwen-code/qwen-code-corebridgeTypes),或
  2. 至少添加一个测试断言二者相等

);
} finally {
entry[flagKey] = false;
if (rerun) void reconcileAfterRoundtrip(entry, target);

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.

[Important] rerun 路径无最大深度限制。虽然被 modelChangeQueue/approvalModeQueue 串行化约束(livelock 极不可能),但在持续快速 model switch 场景下可能累积多层 pending status read。

建议加 retries 参数设上限(如 3 次),超出后 log 告警:

const reconcileAfterRoundtrip = async (
  entry: SessionEntry,
  target: 'model' | 'approvalMode',
  retries = 0,
): Promise<void> => {
  // ...
  finally {
    entry[flagKey] = false;
    if (rerun) {
      if (retries >= 3) {
        writeStderrLine(`[reconcile] ... action=rerun_exhausted retries=${retries}`);
      } else {
        void reconcileAfterRoundtrip(entry, target, retries + 1);
      }
    }
  }
};

if (peer.sessionId === entry.sessionId) {
continue;
}
peer.currentApprovalMode = response.current;

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.

[Important] 直接写入 peer cache 但没有 bump peer.approvalModePublishGeneration。虽然不是功能 bug(peer 的 reconcile 会看到 cache == agent truth 然后 no-op),但违反了 generation counter 的语义("每次 cache 变更都 bump")。

建议要么也 bump generation 保持一致,要么加注释说明此处刻意不 bump 的原因。

// the HTTP client thinks the mode changed while the cache and
// SSE bus still show the old value.
throw new Error(
`Agent returned unknown approval mode: ${JSON.stringify(response.current)}`,

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.

[Important] 这个新增的 throw 路径(agent 返回 unknown approval mode 时抛异常)缺少对应的测试。现有 drops unknown agent-returned approval mode 测试只覆盖了 reconcile 路径的 unknown mode 丢弃,不是此处的 response 验证。

建议补充测试:FakeAgent 的 approval_mode ext 返回 { current: 'super-yolo' },断言 setSessionApprovalMode rejects 且无 approval_mode_changed 事件。

const model = value['currentModelId'];
const mode = value['currentApprovalMode'];
return (
(model === null || typeof model === 'string') &&

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.

[Important] validator 只检查 typeof model === 'string',允许空字符串通过。reducer 的 != null guard 会把 "" 写入 state.currentModelId。虽然 bridge 侧有 non-empty 检查(空字符串到不了 wire),但 SDK 作为最后防线应更严格。

建议改用已有的 isNonEmptyString

(model === null || isNonEmptyString(model)) &&
(mode === null || isNonEmptyString(mode))

@chiga0 chiga0 merged commit a666f90 into daemon_mode_b_main Jun 8, 2026
36 checks passed
@chiga0 chiga0 deleted the feat/daemon-sidechannel-state-layer branch June 8, 2026 15:05
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.

4 participants