fix(desktop): make model and thinking-level changes instant without success toasts - #3749
Conversation
Astro-Han
left a comment
There was a problem hiding this comment.
I reviewed this head and found blocking issues.
[P2] Stop cannot cancel the pre-send model-setup flight
app-shell-chat-actions.ts:493-508 arms the turn before waiting for settleSessionModelSettings. Stop at that point only calls sessions.stop, which returns immediately when no Host turn exists, so the original send still proceeds after settings settle despite user cancellation.
[P2] Active-Turn follow-up bypasses the settlement barrier
app-shell.tsx:1940-1952 enqueues follow-ups directly without going through the settling actions.send path. A model mutation failing to settle still leaves the follow-up queued under the old model, violating the "latest mutation silently settles before immediate send" contract.
Checks on 3686d2aaf are test: success.
简体中文
存在预发送取消与跟随路径两项阻断。|
Addressed both blocking paths in ee34dd0.
Added regression coverage for local cancellation, preserved Host stop behavior, successful follow-up settlement, and failed settlement. Desktop tests pass 1496/1496; typecheck, Biome, and the production build also pass. |
Astro-Han
left a comment
There was a problem hiding this comment.
Update on ee34dd0026:
I found blocking issues that persist/drift in this head.
[P1] Follow-up can still start a new root after Stop
A follow-up submitted on an active turn waits for settings, then submitFollowUpAfterModelSettings has no cancellation identity. If the user presses Stop during the wait, preSendSettingsRef is cleared but the follow-up waiter is not cancelled — it still enqueues and the idle next_turn root starts.
[P2] Root/follow-up dispatches without re-checking surface owner
After the settings wait, the dispatch does not re-verify surface owner. Switching from surface A to B during the wait still sends with A's intent.
[P2] Thinking intent rolls back another client's new model
A stale thinking intent holding an old (model, thinking) tuple can overwrite a newer model B committed by another client.
Checks on ee34dd0026 are test: failure (slash-menu unrelated flake but still not green) — not green.
简体中文
存在跟随提交与所有者/模型回滚三项阻断。|
Addressed the latest settlement/readiness findings in b1ae2a7.
Added regression coverage for all three races. Desktop tests pass 1516/1516; production build, typecheck, Biome, and diff checks also pass. |
Astro-Han
left a comment
There was a problem hiding this comment.
I reviewed this head and found blocking issues.
[P2] use-session-model-settings-intent.ts:118 — model equality used as ordering token
A thinking write M/high commits, another client then commits M/low and the new catalog snapshot arrives before the older IPC response. Because model is unchanged, the stale M/high is accepted and stamped at the current revision, leaving the overlay wrong if the best-effort refresh fails. Carry a causal token (host revision) through mutation result and catalog.
[P2] chat-model-switcher.tsx:231 — accessibility loses current/success state after toast removal
Fixed aria-label replaces visible label, current check is aria-hidden, no selected state, and status only announces warnings. After removing the toast, AT cannot read the current model or hear success.
[P3] app-shell.tsx:1773 — cancellation epoch tombstones accumulate
Every Stop inserts an epoch, but clearSessionRendererState never cleans it, leaving renderer-lifetime tombstones.
Checks on b1ae2a754f are test: success.
简体中文
存在两项正确性与一项可及性阻断,另有一项生命周期 P3。|
Thanks for the thorough work here — the test coverage in particular is careful. But I'd like to redirect the approach before this goes further. The seam already exists. Two of the three reported problems need no new machinery. Stacked toasts and the wrong Part of this belongs to #3347, which we have not decided to do. Settle-before-send, Stop cancelling an in-flight settings write, and gating follow-ups on settlement all address "the send may use a configuration the Host has not committed yet". That is #3347's problem, and it puts renderer-side ordering in front of Host turn admission. Please drop those here. Suggested shape — this should come out as a net deletion:
Two smaller things worth closing while you are here: 简体中文感谢你细致的工作,测试尤其扎实。但在继续之前我想调整一下方向。 这个 seam 已经存在。 三个问题里有两个不需要新机制。 toast 堆叠和 有一部分属于 #3347,而我们还没有决定要做它。 发送前 settle、Stop 取消在途的设置写入、follow-up 进队列前先 settle,解决的都是「发送时用的配置可能还没提交到 Host」——那是 #3347 的问题,而且它让渲染层排在了 Host 的 Turn 准入之前。请从本 PR 移除。 建议的形状,结果应该是净删除:
另有两处顺手可以收掉: |
|
Following up with the full reasoning behind that suggestion — it is a fairly large redirect, so it seems only fair to show the whole picture rather than just the conclusion. Two mechanisms for one factDesktop currently has two different answers to "a session setting write is in flight", and which one you get depends on which setting you touched.
Every one of these five settings goes through the same shape on the Host side — the same flowchart TB
subgraph A["useSessionSettingIntent — 144 lines, generic"]
A1["Plan mode"] --> AI["desired / committed<br/>committedAtCatalogRevision / inFlight"]
A2["Orchestration mode"] --> AI
AI --> AO["overlay: shows immediately, latest wins"]
AI --> AW["write loop: re-reads desired<br/>rolls back to committed on failure"]
AW --> AR["refreshCatalog, retire overlay by catalogRevision"]
end
subgraph B["pending registry + toast"]
B1["Model"] --> BR["keysRef: Set of sessionId"]
B2["Thinking level"] --> BR
B3["Permission mode"] --> BR2["keysRef: Set of key"]
BR --> BS["pendingSessionModelBySession"]
BR2 --> BS2["pendingPermissionModeBySession"]
BS --> BD["disabled + spinner"]
BS2 --> BD
BR --> BX["repeat click dropped"]
BW["write succeeds"] --> BT["toast.success"] --> BF["refreshSessions"]
end
What the difference looks like in timesequenceDiagram
autonumber
participant U as User
participant C as Control
participant S as settings-actions
participant R as pending registry
participant H as Runtime Host
Note over U,H: today, model / thinking / permission
U->>C: pick B
C->>S: setSessionModel(B)
S->>R: keysRef.add(sessionId)
S->>S: setPendingBySession(true)
Note over C: disabled + spinner, still shows A
U->>C: quickly pick C
C->>S: setSessionModel(C)
S-->>C: has(sessionId), returns early — C is lost
S->>H: sessions:setModel(B)
H-->>S: SessionSummary(B)
S->>S: latestAssistantModelId(messages) for the "from" label
S->>U: toast "A to B"
S->>H: refreshSessions()
S->>R: delete + setPendingBySession(false)
sequenceDiagram
autonumber
participant U as User
participant C as Control
participant I as useSessionSettingIntent
participant H as Runtime Host
Note over U,H: today, plan / orchestration
U->>C: pick B
C->>I: request(sessionId, B)
I->>C: overlay = B, shown immediately, not disabled
I->>H: write(B)
U->>C: quickly pick C
C->>I: request(sessionId, C)
I->>I: desired = C, no second write started
I->>C: overlay = C
H-->>I: committed(B)
I->>H: write(C)
H-->>I: committed(C)
I->>H: refreshCatalog()
I->>I: newer catalogRevision retires the overlay
The second diagram is the behaviour your issue asks for. It is already in the tree. What disappears if the three settings move over
Nothing new is added: For permission mode, the bypass confirmation moves to the call site — confirm first, then What we give upThe control is no longer disabled while a write is in flight. That is the point of your issue, so I think it is a gain — but it is a real behaviour change and worth stating plainly. Two smaller things in the same area
"Why can't I change this right now" is derived in three places — Neither is a blocker. They are cheap to close while the area is open, and both go away on their own once the three settings share one path. Happy to talk through any of this if you see it differently — you have been in this code more recently than I have. 简体中文补上建议背后的完整理由。这是一次幅度不小的方向调整,只给结论不太公平。 一个事实,两套机制Desktop 现在对「会话设置写入在途」有两个不同的答案,取决于你动的是哪个设置。
这五个设置在 Host 侧走的是完全相同的形状——同一个 上面两张时序图分别对应这两条路径。第二张就是你 issue 里想要的行为,它已经在仓库里了。 三个设置迁过去之后会消失的东西
不需要新增任何东西: 权限模式的 bypass 确认弹窗移到调用点——先确认,再 让出的东西写入在途时控件不再被禁用。这正是你 issue 的目的,所以我认为是收益,但它确实是一次行为变化,应该明说。 同一片区域的两个小问题
「为什么现在不能改」在三个地方各推导了一遍—— 两个都不阻塞。趁这块打开时收掉很便宜,而且三个设置合并到同一条路径后它们会自行消失。 如果你有不同看法,随时聊——这块代码你比我更熟。 |
071ec88 to
3fdf585
Compare
Astro-Han
left a comment
There was a problem hiding this comment.
Update on 3fdf5857a0:
[P2] Model intent fire-and-forget allows send on stale model
app-shell.tsx:3030-3032 fires modelIntent.request() without waiting, and app-shell-chat-actions.ts:349-350 send() no longer waits for settleSessionModelSettings. The Host setModel IPC can still be pending when sessions.send runs, so the turn can start with the old model A even though the UI shows B.
Fix: restore a per-session settlement barrier between model change and send, or carry expected model/version into send and retry on mismatch.
Checks on 3fdf5857a0 pending — code is NO-GO independent of CI.
简体中文
存在模型意图与发送竞态导致的错模型启动。8b55108 to
9a20b96
Compare
Astro-Han
left a comment
There was a problem hiding this comment.
I reviewed this head and found a blocking issue.
[P2] Model selection optimistic write races Session Send
app-shell.tsx:3080-3087 writes model setting optimistically and returns immediately; app-shell-chat-actions.ts:502-516 sends Session without waiting. turn.start may capture old model before session.configuration.update commits — user sees B but first turn runs with A. Shared admission only serializes enqueued ops, not cross-request ordering.
Fix: restore per-Session settlement barrier or add expected-revision fence to turn.start; add delayed-write regression test.
简体中文
模型切换与发送存在竞态。Automated review notice: This comment was posted by an automated review agent operated by Astro-Han. It is not an independent human review and does not replace one.
The race described here is a valid configuration-ordering concern, but it is explicitly out of scope for #3745 and tracked by #3347. Earlier review on this PR specifically asked to remove settle-before-send, Stop cancellation, and follow-up gating because renderer-side ordering should not own Host turn admission. The current head and its architecture test follow that direction. |
There was a problem hiding this comment.
Retracting this review. It contradicted my own earlier feedback on this PR: on 2026-08-25 I asked for a causal token carried through the mutation result and catalog, and flagged that a stale thinking intent holding an old (model, thinking) tuple can overwrite another client's newer model. The compound modelConfigurationIntent and the sessions:setModelConfiguration operation are the response to exactly that, so asking to split them back apart would reintroduce the bug I asked to have fixed. The inline comment about thinking writes carrying the model has been deleted for the same reason.
The remaining inline comments stand on their own; I will re-issue anything still outstanding as a fresh review.
Contradicted my own earlier guidance on this PR; re-reviewing.
Astro-Han
left a comment
There was a problem hiding this comment.
Re-reviewing after retracting my previous review, which contradicted my own earlier feedback here.
To be explicit so the retraction is not ambiguous: the compound modelConfigurationIntent and the sessions:setModelConfiguration operation are the right answer to what I raised on 2026-08-25 — a stale thinking intent carrying an old (model, thinking) tuple overwriting another client's newer model, and the need for a causal token through the mutation result and catalog. committedAtCatalogRevision is that token. Please disregard the earlier suggestion to split them back into independent controllers; #3745's "independent" wording predates that feedback.
The direction of the PR is right and the deletions are real: both change registries, both pending prop chains, the switcher's pending/loading, the success toasts and their copy in two languages are gone.
Two things on convergence, plus the inline notes.
Side chat still runs its own mechanism. use-quote-companion.ts:217,817-833 keeps a permissionModePending boolean with discard-on-reentry, and this PR adds a permissionChanging string to conversation-copy.ts — reviving in a new file the same copy just deleted from shell-copy.ts. That is the "one fact, two mechanisms" this PR sets out to remove. If side chat is deliberately out of scope, worth saying so in the body, and the new copy should not land.
One piece of scope is outside #3745. The catalog-diff teardown effect and session-catalog-removals.ts are not asked for by the issue, and they are where the widest inline note below sits. What this PR needs from that effect is the two new intent overlays cleared.
AI use: Claude Code assisted with source and issue investigation; the analysis and conclusions are my own.
|
On the send-ordering race: you are right, and that automated finding is non-blocking here. Renderer-side settlement was removed at my request because ordering in front of Host turn admission is the wrong place for it, and #3347 is open for exactly this. If we fence it, I agree it should be a Host-side expected-revision check on Separately, my earlier review today was retracted: it asked you to split the compound model/thinking intent back apart, which would have undone the stale-tuple fix I asked for on 08-25. Sorry for the churn — the re-review reflects the corrected position. |
|
Addressed the review summary in e7386e5. Side chat permission changes now use the same useSessionSettingIntent seam: the selected mode is projected immediately, rapid changes coalesce latest-wins, and a stale first response cannot replace the newer overlay. Removed permissionModePending and the newly added permissionChanging copy. The compound modelConfigurationIntent and sessions:setModelConfiguration remain intact. Local verification passes: Desktop 1532/1532, Desktop typecheck, production build, full-repo Biome, and diff checks. |
|
Thanks for continuing to converge this toward the existing Before another review round, could you rebase onto current This issue should ideally land as a net simplification around the existing intent owner. Once the refreshed diff contains only that end state and the necessary behavioral coverage, we should be able to review and merge it quickly. Please push back where an apparently adjacent path is required to preserve the same setting-intent invariant. AI-assisted review disclosure: Codex assisted with the cumulative-diff inventory; Astro-Han reviewed the accepted direction and owns this recommendation. |
8e5c596 to
d27f6fc
Compare
|
Hi! @Astro-Han Thanks for the scope review. I rebuilt this PR on top of the latest Kept because they are directly required by #3745 or by bugs identified during review:
Removed from this PR:
The diff is now 25 files ( Verification:
Please review from |
Astro-Han
left a comment
There was a problem hiding this comment.
Thanks for carrying this through several rounds of review and simplification. On exact head d27f6fc, the model and thinking-level interaction now converges on one compound configuration path, cleanup follows explicit deletion facts, the dead model-only IPC and brittle source-text tests are removed, and the no-session persistence guard is preserved. The previous review findings are addressed, and I found no remaining P0–P3 issue.
The exact-head test check is currently queued, so that remains the normal merge-time gate.
This was an AI-assisted review using Codex across multiple review rounds. I independently verified the current head, the addressed findings, production ownership, and merge state, and I own this approval.
|
Merging after the CI pass! |
The model picker now exposes radio semantics, but the accessibility E2E still queried plain menu items. Assert menuitemradio so the test follows the rendered accessibility tree. Generated-by: OpenAI Codex
Model picker options now expose menuitemradio semantics. Update the affected Storybook play assertions and verify the selected thinking level through aria-checked. Generated-by: OpenAI Codex
Generated-by: OpenAI Codex
Generated-by: OpenAI Codex
f74f3e6 to
5dff845
Compare
Resolve the renderer architecture inventory against the merged AppShell source. Generated-by: OpenAI Codex
Generated-by: OpenAI Codex
|
Hi @Astro-Han , I’ve pushed the latest updates, resolved the merge conflicts with Since these commits were added after the previous approval, could you please take another look at the updated diff and re-approve if everything looks good? Thanks! |
# Conflicts: # apps/desktop/renderer-architecture.json # docs/astryx-surface-file-inventory.md
Astro-Han
left a comment
There was a problem hiding this comment.
Re-reviewed the delta from d27f6fc3 (where I approved on Aug 31) to 96f1682a. Confirming first that all four of my open threads are genuinely closed in code, not by assertion — I've resolved them:
sessions:setModelis gone; onlysessions:setModelConfigurationremains (runtime-host-session-catalog-ipc-main.ts:209).model-settings-visual-contract.test.tsis deleted, so nothing asserts design intent by regex over source any more.session-catalog-removals.tsno longer exists, andclearSessionRendererStateis now reached only fromapp-shell-effects.ts:190/250and the explicit row action — absence and deletion are no longer the same fact.- The no-session permission branch is no longer a hazard:
setNewTaskPermissionModeresolves touseNewTaskChoice's synchronous setter, so there is no async persistence to reorder.
The delta itself holds up. The features/session-settings/ folder with ports.ts + services-context.tsx + a desktop adapter is the same shape features/goals already uses, and packages/ui already carries non-visual state hooks (use-composer-draft, use-transcript-projection, daily-review-view-state), so extracting the intent hook there follows precedent rather than inventing a home. Generalizing it to multi-channel is what lets the main chat and the side chat share one seam, and it is the replacement for the deleted app-shell-session-settings-actions.ts — one mechanism swap, so I'm not asking for a split.
Approving at 96f1682a. Two P2s and one P3.
P2-1 — the side chat gains a Full access confirmation it never had, and the description does not say so. On main, the main chat confirms bypass (app-shell-session-settings-actions.ts:122-127) while the side chat writes it straight through (use-quote-companion.ts:952). Routing both onto one intent seam makes closing that gap part of this change — that's the right call. But Summary line 5 reads as a sequencing note about an existing confirmation ("happens before the permission intent starts"), so a reader of the PR, the squash message, or the release notes cannot learn that unconfirmed Full access in the side chat was possible before and is not now. This is the only security-relevant change in the PR; please give it its own line.
P2-2 — the new Full access coverage tests a three-line helper instead of the path. declining Full access does not persist the side-chat permission mode calls requestPermissionModeWithConfirmation directly with two fake closures. It never mounts useQuoteCompanion, even though the same file already sets up createRoot, WorkbarServicesProvider, and createFakeWorkbarServices for the retry tests. The regression it exists to prevent — setPermissionMode (line 1004) calling requestPermissionMode(id, mode) directly, which is exactly what main does today — leaves this test green. Asserting through the hook is cheap here and is the difference between proving the helper's if and proving the confirmation is on the path.
P3 — the PR roughly doubled after approval and the Summary no longer describes it. 25 files / +759/-929 at d27f6fc3, 47 files / +1734/-1108 now. Added since: the session-settings feature folder and its services wiring, the packages/ui extraction, and the side-chat confirmation. Each is defensible individually, as above, but the Summary still describes only the instant-feedback and toast removal. Naming the structural moves would let the next reader see the same PR I just re-read.
AI use: re-reviewed with Claude Code (comparing each head's diff against its own merge-base to isolate PR-owned changes from what the merges pulled in). I verified each of the four resolved threads against the code at 96f1682a, the pre-existing side-chat gap on main, the confirmation's wiring through WorkbarHost → WorkbarSurface → QuoteCompanionPanel, and the new test's actual entry point myself.
|
Follow-up to my re-review — I went back to answer why the diff doubled, and two of the three blocks are not what I first assumed. Neither of these is a merge condition; the second in particular is a direction, not a request. The joining-the-DI-architecture block is not invention: The At One consequence points the wrong way for where this codebase is heading. Dropping the generalization removes the On the plan-mode split, I'd withdraw the way I framed it. I said three channels reach the Host through The more general representation is the injected closure: the port can be expressed as one, the reverse is not true, and plan mode proves it. So the lowest-entropy shape is for the wrapper to receive four The fair counter is that this is the one thing that would not match the other seven features, and that if session-settings later follows goals and module-hub below AppShell, the context port becomes load-bearing. I don't think that outweighs a four-channel boundary with one channel outside it today, but it is a real trade and it is yours to make. Both of these are structural, and this PR has already doubled once. I would not do either here. They belong with the next round of controller-scoping work, alongside #4316 and #4315. AI use: audited with Claude Code (full read of the shared hook, the desktop wrapper's four channels, the seven sibling feature ports and adapters, and AppShell's direct bridge usage). I verified the pre-PR hook's arity and instantiation sites, the absence of cross-channel coordination, and that |
# Conflicts: # apps/desktop/renderer-architecture.json # apps/desktop/src/main/__tests__/app-shell-session-settings-actions.test.ts # apps/desktop/src/renderer/app-shell-session-settings-actions.ts
Resolve the generated Astryx surface inventory against both branches.\n\nGenerated-by: OpenAI Codex
Regenerate the renderer architecture and Astryx surface inventories from the merged source tree.\n\nGenerated-by: OpenAI Codex
me2seeks
left a comment
There was a problem hiding this comment.
Review pass over the intent machinery, the IPC seam, and both surfaces (main shell + side chat). Nothing blocking from my side — the design holds up: per-channel latest-intent workers converge rapid changes, rollback on terminal failure goes to the last committed value, the bypass confirmation captures its owner before awaiting, and the sessions:setThinkingLevel bridge method is correctly retained (still used by the slash-command e2e). Removing the success toasts and the pendingPermissionModeBySession/pendingSessionModelBySession maps is a clean net reduction.
Three boundary observations, all non-blocking:
-
Side-chat bypass confirmation can race a turn start. In
use-quote-companion.ts,requestPermissionModeWithConfirmationawaitsconfirmBypass()and then callsrequestPermissionMode(id, mode)without re-checkingturnInFlight. The picker is disabled while streaming, but a queued message can start a turn while the confirm dialog sits open, letting a mid-turn permission write through. The main-shell flow re-validatesisOwnerActive(owner)after the await; the companion flow has no equivalent re-check. If the Host itself rejects mid-turn mode changes this is moot — otherwise consider re-checking after the confirm resolves. -
Overlay retirement depends on catalog-revision ordering.
committedAtCatalogRevisionis sampled when the write resolves, andreconciledrops the overlay on any strictly newercatalogRevision. BecauserefreshCatalog()is fire-and-forget, an unrelated catalog bump (another session's change) that is observed before a snapshot actually containing this commit could retire the overlay early, transiently flipping the control back to the old value. Self-heals on the next refresh; only matters if catalog revisions are not a commit-ordered counter over the catalog contents. -
Nit: in
app-shell-session-settings-actions.ts, thepending === falsebranch ofhandleModelPickstill shows thecatalogLoad"Switching model…" toast for 4s even though nothing is in flight (this is the pre-mount race path, not a real switch). A silent no-op or the failure copy would be less misleading.
Epoch/inventory churn (renderer-architecture.json, surface inventories) intentionally ignored — CI gates that.
|
Thanks for the review. I checked these observations against the current head:
Therefore I’m not changing this PR for observations 1 or 3, and will track observation 2 separately. |
me2seeks
left a comment
There was a problem hiding this comment.
Thanks for the careful check — resolving all three:
-
Side-chat confirmation race — accepted. With the Runtime Host rejecting configuration transitions during an active Turn and the ordering work tracked in #3347, this cannot cross the permission boundary. Non-issue here.
-
Catalog-retirement ordering — thanks for reproducing it; a focused follow-up with a per-target-session causal signal plus regression coverage sounds exactly right.
-
catalogLoadtoast nit — retracted, with apologies. I verified head2ff0ab249:app-shell-session-settings-actions.tsis deleted by this PR and nohandleModelPick/catalogLoadpath exists. My note was a misread on my side, not a real referent.
Nothing outstanding from me. Approving — the channels generalization discussion elsewhere in the thread is a direction question, not a merge condition.
|
@me2seeks I believe this PR is ready to merge. Observation 2 is a real but non-blocking edge case involving partial multi-Host catalog refreshes. I’m currently working on the fix and will open a separate follow-up PR for it. |
…ed-quiescence machinery (apache#3349) Direction decision from review: nothing requires a grant to wait out the live turn. Tools read the boundary live on every dispatch and every frozen consumer (client-capability admission, the plan prompt) fails closed against a wider boundary, so immediate widening can only under-grant inside the turn; a wider parent only relaxes descendant containment; and the Desktop picker sends a permission-only patch, so the mixed-update session_busy price never reaches it. Both directions now share one commit path on the admission-mutation tail: fencing (shell termination) and descendant constraining run only when the change actually narrows someone — a narrowing on its face, or a permission request repairing a descendant the committed parent will not contain (a bypass target contains every local descendant, so a grant touches nothing). The queued-widening machinery the direction existed for is gone with it: runSessionQueuedQuiescentMutation and the kernel's third mutation semantics, the admission gate half of the admission barrier (which returns to the plain mutation tail), claimSeq and the frontier, the quiescence waiters, and the interleaving test file — roughly 220 production lines. runSessionAdmissionMutation, runSessionQuiescentMutation, the busy error and narrowsExecutionAuthority all keep their other callers; widensExecutionAuthority loses its last one. Lineage findings from the same review land here: - A composition-only update no longer reroutes through the constraining treatment when a descendant legitimately exceeds the parent (an approved child expansion was permanently classified as unconverged: every parent update killed shells, projected the child down, and a live model switch rejected session_busy right after apache#3749 made model changes instant). The transition request carries an explicit permissionTransition intent; only a permission request constrains. - constrainDescendantBoundary re-reads the descendant boundary at constraint time instead of trusting the pre-commit snapshot: nothing serialises the child, and a child that widened into the window was judged already contained, skipped, and its shells resumed on the stale value. - listLinkedDescendantSessionIds queries children per level through the store's subagentParentSessionId filter instead of scanning every session on each walk. The queued-path tests are rewritten to the immediate semantics the review asked for: a mid-turn Ask→Bypass grant is observed by the live turn's next dispatch, shells are not killed, descendants are untouched, a mixed widening with a live run rejects session_busy, and the idle successor-turn case from the issue stays. Generated-by: ZCode (Z.ai GLM)
Optimistic Session-setting overlays (model, thinking level, permission, orchestration) were retired on the next renderer-wide catalog revision. Any unrelated Session changing anywhere bumped that revision, so an overlay could disappear before the target Session's snapshot had observed the committed write, and the UI briefly showed the old value again. This was the open review note from #3749. Each mutation already returns the Session revision it committed. That revision is now carried through the Desktop projection to the renderer, and the overlay is kept until the target Session's own catalog revision reaches it. Intent channels that return a revision are typed to require a matching Session-revision observer, so a new channel cannot silently fall back to the catalog-wide rule. Plan mode stays on catalog-wide retirement: its pending-proposal path completes through `abandonPlanProposal`, which returns `PlanSessionState` rather than a Session revision. Moving it needs a separate contract change and is tracked as follow-up. No Runtime Host wire protocol or persistence format change. Generated-by: Codex
Summary
useSessionSettingIntentcontrollers. Controls show the latest requested value immediately, rapid changes converge on the latest intent, terminal failures roll back to the last committed value, and newer catalog revisions retire optimistic overlays.features/session-settingsservices boundary and shared UI intent hook, replacing the deleted AppShell settings-action module and wiring the main chat and side chat through one seam.Fixes #3745
Verification
useQuoteCompanionpath and verifies that declining Full access does not call persistence.Visual verification
Before
Rapid thinking-level changes created stacked success toasts with obsolete intermediate values.
After — instant, no spinner
Model and thinking-level controls reflect each selection immediately without a spinner, disabled flicker, or success toast.
Static final state (
GPT-5.6 Sol / Low):AI use
Tool(s) and scope: OpenAI Codex implemented the session-setting intent integration, transient-feedback cleanup, regression and accessibility coverage, and verification. A
Generated-by: OpenAI Codextrailer is present on each materially affected commit.Checklist
Does this PR entail a user-facing behavior change?