feat(daemon): in-session model switch reaches the bus (A1)#4546
Conversation
Implements A1 from the side-channel coordination design (#4511): a /model slash command or plan-mode model switch now reaches attached clients, where previously only the HTTP POST /session/:id/model path published model_switched. Transport (per design v7): current_model_update is NOT an ACP SessionUpdate variant (the type is the external @agentclientprotocol/sdk union — it has current_mode_update but no model equivalent), so the agent emits the change over the agent->bridge extNotification side-channel. - Agent: Session.setModel emits a `qwen/notify/session/model-update` extNotification after switchModel resolves (success-only; captures the previous model id). Fire-and-forget — a failed notification never fails the switch. - Bridge: BridgeClient.extNotification demuxes it to a model_switched bus event (currentModelId -> data.modelId), SUPPRESSED while the bridge is driving its own model roundtrip (entry.modelRoundtripInFlight, set around setSessionModel / applyModelServiceId) so the HTTP path — which also flows through Session.setModel — does not double-publish. Structured demux log records promoted / suppressed / dropped decisions. Scope: this is the core A1 path + suppress + observability. The §2.2 post-roundtrip reconciliation and the timeout-race staleness check (for the rarer concurrent-in-session / timed-out-then-late races documented in the design) are a tracked follow-up. Tests: agent emits the notification on success and not on failure; bridge promotes it to model_switched when idle and suppresses it during a bridge roundtrip. acp-bridge 302 pass.
📋 Review SummaryThis PR implements A1 from the side-channel coordination design (#4511), enabling in-session model switches (via 🔍 General Feedback
🎯 Specific Feedback🟢 Medium
🔵 Low
✅ Highlights
|
- Update the extNotification JSDoc to list both recognized methods
(mcp-budget-event + model-update).
- Drop previousModelId from the model-update notification — nothing consumed
it end-to-end (dead data); model_switched is {sessionId, modelId}.
- setSessionModel: publish model_switched INSIDE the modelChangeQueue work
callback (while modelRoundtripInFlight is still true), mirroring
applyModelServiceId, so the agent notification can't slip through after the
flag clears if transport ordering ever changes.
acp-bridge 302 pass; typecheck + lint clean.
|
This PR is part of a conflict group with PR #4476, PR #4507, PR #4520, and PR #4545. Key shared functions:
Recommendation: This PR adds in-session model switch which shares event bus and session management code with other PRs. Coordinate with the group before merging. This conflict was detected automatically by CodeScope skills — structural analysis of call graphs and file modifications. |
|
This PR is part of a conflict group with PR #4476, PR #4507, PR #4520, and PR #4545. Key shared functions:
Recommendation: This PR adds in-session model switch which shares event bus and session management code with other PRs. Coordinate with the group before merging. This conflict was detected automatically by CodeScope skills — structural analysis of call graphs and file modifications. |
wenshao
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅ — qwen3.7-max via Qwen Code /review
Add the three branch tests wenshao flagged: malformed model-update params (non-string ids → early return, no emit), unknown sessionId (dropped, not buffered), and originatorClientId propagation (a model-update during an in-flight prompt inherits activePromptOriginatorClientId on the promoted model_switched).
|
Note: this PR merged before I pushed the applyModelServiceId publish-guard fix for your suggestion above, so it landed as a small follow-up PR instead. |
Squashed feature work from daemon_mode_b_main branch, rebased onto latest main to establish proper merge-base and clean PR diff. Original commits: - perf(core): F2 cleanup PR A — R9/W11/W12/R10 (post-merge follow-ups) (#4411) - refactor(acp-bridge): F1 test split — lift bridge.test.ts (6861 LOC) to acp-bridge (#4445) - fix(core): F2 cleanup PR B — self-heal observability (W133-a + W134) (#4460) - feat(sdk/daemon-ui): unified completeness follow-up to #4328 (#4353) - docs(serve): v0.16-alpha known limits + SDK QWEN_SERVER_TOKEN env fallback (PR 27) (#4473) - docs(deploy): local launch templates for v0.16-alpha (PR 30a) (#4483) - feat(daemon+sdk): cross-client real-time sync completeness (#4484) - feat(serve): add POST /session/:id/recap (#4504) - feat(daemon): add voterClientId to permission_resolved (A4) (#4539) - feat(serve): --allow-origin <pattern> CORS allowlist (T2.4 #4514) (#4527) - feat(daemon): in-session model switch reaches the bus (A1) (#4546) - feat(serve): prompt absolute deadline + SSE writer idle timeout (#4514 T2.9) (#4530) - Feat/daemon react cli (#4380)
* feat(daemon): in-session model switch reaches the bus (A1) Implements A1 from the side-channel coordination design (#4511): a /model slash command or plan-mode model switch now reaches attached clients, where previously only the HTTP POST /session/:id/model path published model_switched. Transport (per design v7): current_model_update is NOT an ACP SessionUpdate variant (the type is the external @agentclientprotocol/sdk union — it has current_mode_update but no model equivalent), so the agent emits the change over the agent->bridge extNotification side-channel. - Agent: Session.setModel emits a `qwen/notify/session/model-update` extNotification after switchModel resolves (success-only; captures the previous model id). Fire-and-forget — a failed notification never fails the switch. - Bridge: BridgeClient.extNotification demuxes it to a model_switched bus event (currentModelId -> data.modelId), SUPPRESSED while the bridge is driving its own model roundtrip (entry.modelRoundtripInFlight, set around setSessionModel / applyModelServiceId) so the HTTP path — which also flows through Session.setModel — does not double-publish. Structured demux log records promoted / suppressed / dropped decisions. Scope: this is the core A1 path + suppress + observability. The §2.2 post-roundtrip reconciliation and the timeout-race staleness check (for the rarer concurrent-in-session / timed-out-then-late races documented in the design) are a tracked follow-up. Tests: agent emits the notification on success and not on failure; bridge promotes it to model_switched when idle and suppresses it during a bridge roundtrip. acp-bridge 302 pass. * fix(daemon): address review on A1 in-session model update - Update the extNotification JSDoc to list both recognized methods (mcp-budget-event + model-update). - Drop previousModelId from the model-update notification — nothing consumed it end-to-end (dead data); model_switched is {sessionId, modelId}. - setSessionModel: publish model_switched INSIDE the modelChangeQueue work callback (while modelRoundtripInFlight is still true), mirroring applyModelServiceId, so the agent notification can't slip through after the flag clears if transport ordering ever changes. acp-bridge 302 pass; typecheck + lint clean. * test(daemon): cover A1 demux defensive branches Add the three branch tests wenshao flagged: malformed model-update params (non-string ids → early return, no emit), unknown sessionId (dropped, not buffered), and originatorClientId propagation (a model-update during an in-flight prompt inherits activePromptOriginatorClientId on the promoted model_switched). --------- Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
What this does
Implements A1 from the side-channel coordination design (#4511). A
/modelslash command or a plan-mode model switch now reaches attached clients — previously only the HTTPPOST /session/:id/modelpath publishedmodel_switched, so an in-session switch left peers' model badge silently stale.Transport (design v7)
current_model_updateis not an ACPSessionUpdatevariant —SessionUpdateis the external@agentclientprotocol/sdkunion, which hascurrent_mode_updatebut no model equivalent. So the agent emits the change over the existing agent→bridgeextNotificationside-channel (the same mechanism used for MCP-budget events today), and the bridge demuxes it to the existingmodel_switchedbus event. No ACP-spec change, no SDK change (consumers keep seeingmodel_switched).Changes
Session.setModel): emitsqwen/notify/session/model-updateafterswitchModelresolves (success-only; captures the previous model id). Fire-and-forget — a failed notification never fails the switch.BridgeClient.extNotification): demuxes it →model_switched(currentModelId → data.modelId), suppressed while the bridge is driving its own model roundtrip (entry.modelRoundtripInFlight, set aroundsetSessionModel/applyModelServiceId). The HTTP path also flows throughSession.setModel, so without suppression it would double-publish. Structured demux log recordspromoted/suppressed/dropped.Scope
This PR is the core A1 path + suppress + observability + tests. The design's §2.2 post-roundtrip reconciliation and the timeout-race staleness check (for the rarer concurrent-in-session and timed-out-then-late races) are a tracked follow-up — they harden edge races on top of this path.
Tests
model_switchedwhen idle; suppresses it during a bridge-driven roundtrip (no double publish).Design
Second implementation PR of #4511 (after A4 / #4539). A2 remains blocked on #4510 (
approvalModeQueue); A5 is thesession_snapshotframe.