fix(desktop): retire model overlays by session revision - #4530
Conversation
Generated-by: Codex
Generated-by: Codex
Generated-by: Codex
Generated-by: Codex
Generated-by: Codex
Generated-by: Codex
Astro-Han
left a comment
There was a problem hiding this comment.
The revision approach is right, and I checked it rather than taking it on faith. metadataVersion increments at a single point (sqlite-session-metadata-store.ts:4985), list and mutation both project it through toDesktopHostSessionSummary, so the revision a write returns and the revision in a later snapshot are the same monotonic sequence. Neither failure mode the comparison could have had is reachable: revision changed but the overlay should stay cannot happen because a change means the record passed the write, and revision unchanged but the overlay should go cannot happen because a write always increments. I also walked the A to B switch path and the failure rollback; both are self-consistent.
Worth saying explicitly: the value-comparison shape (does the catalog row equal what I committed) would have been smaller but has a real hole, a third party writing a fourth value right after would strand the overlay forever. The revision plumbing closes that, so it is not overhead.
[P2] The new rule is installed on one channel, and the old one stays live for the other three
permissionMode, planMode and orchestrationMode still return a boolean and have no catalogSessionRevision, so they keep going through session-setting-intent.ts:154's catalogRevision <= committedAtCatalogRevision. That is the rule this PR just showed to be wrong. The same flash is reachable on the permission-mode chip, including bypass, by exactly the trigger described here.
This is pre-existing rather than introduced, but the PR leaves two retirement authorities standing where there was one.
The data is already in hand: bridge-contract.d.ts:1222 and :1233 already type setPermissionMode and setOrchestrationMode as returning DesktopSessionSummary, and at runtime they go through toDesktopHostSessionSummary, so revision is there. Only ports.ts:30-31 still says SessionSummary. Changing those two return types lets both channels return { committed, sessionRevision } and add one line each, after which SessionSettingIntentWriteResult's boolean branch, the committedAtSessionRevision !== undefined branch, committedAtCatalogRevision and options.catalogRevision all delete. Net removal.
planMode's write is the injected commitPlanMode, which is policy rather than transport, so leaving it behind is defensible. If it stays, please say why in the body.
[P3] Two small things
desktop-session-projection.ts:205 has revision: session.revision right after ...session, and the input type already carries revision, so the line is a no-op. The same-looking line in runtime-host-session-catalog-ipc-main.ts:409 is necessary, since projectSessionCatalogSummary drops revision, so do not remove both.
Nothing enforces that a channel providing an object-shaped write also provides catalogSessionRevision. Get that pairing wrong and observedRevision is permanently undefined, which reads as "the snapshot has not caught up" and pins the overlay forever, silently. Today every production path self-heals, so this is a shape worth making unrepresentable rather than a live defect.
Next step
The one thing I would want before merge is either the P2 closure or a sentence in the body saying why three channels keep the old rule.
This changes visible chip behavior and adds a new failure mode (never retiring) that CI cannot see, so two manual checks matter more than the rest: with two Hosts, switch the model on Host A, break Host A's list while Host B refreshes, and confirm the chip holds the new value. Then, single Host, switch model and thinking level normally and confirm the chip actually does retire to the authority value rather than staying overlaid; rapid A to B to C should settle on C.
Evidence boundary: read only, no build, no tests, and I did not run the app or reproduce the flash. The mechanism claim rests on the stale-row retention in reconcileRuntimeHostSessionCatalog plus the unconditional revision bump in commitSessions. I traced the single increment point in storage but did not audit every header write path for one that bypasses it.
AI-assisted review: drafted with Maka.
Generated-by: Codex
|
Implemented the P2 closure in
I also applied both P3 suggestions:
One correction to the proposed simplification: changing permission and orchestration does not let us delete the boolean/ On CI coverage, the tests now explicitly cover retention across a partial Host catalog, retirement at each target Session revision, a mismatched orchestration response, and rapid A→B→C settlement. I did not repeat the full rendered two-Host manual scenario; the controller regression test exercises the stale-row reconciliation state directly, while the rapid path is covered in the shared intent tests. Verification passed for build, formatting, lint, typecheck, renderer architecture, and all focused suites. The final root run had one unrelated |
Astro-Han
left a comment
There was a problem hiding this comment.
All three points from my earlier pass are addressed, and I checked the closure rather than taking the reply on faith, so this is good to merge with two P3 notes.
On the P2: permissionMode and orchestrationMode now return revision receipts (use-session-setting-intent.ts:104-114, :121-132), and the revision is real at runtime, not just in the types. Both IPC handlers go through updateConfiguration (runtime-host-session-catalog-ipc-main.ts:318-328) into toDesktopHostSessionSummary, and runtime-host-client.ts:1000-1016 returns a SessionCatalogProjection whose revision and orchestrationMode are both required and decoder-enforced (protocol/session-catalog.ts:239-247, :1023-1028). So the stricter orchestration check cannot misfire on a Session sitting at the default mode, which was the failure I went looking for. The Plan justification also holds: commitPlanMode (app-shell.tsx:1006-1037) leaves Plan through abandonPlanProposal on the pending branch, which returns no Session summary. Your correction about net removal is right, and I would add that the side-chat permission channel (use-quote-companion.ts:266-292) is a second boolean consumer, though its catalogRevision is a private counter that only its own refresh advances, so it is not exposed to the failure this PR fixes.
Both P3 items are closed: the redundant projection line is gone, and the channel shape is now a discriminated union with a @ts-expect-error guard.
One thing I checked that is worth recording: sessions and catalogRevision are committed as one snapshot (session-catalog-state.ts:55-58), so there is no window where the list carries a newer Session revision while catalogRevision stands still and the reconcile effect never runs.
P3: catalogSessionRevision now backs three channels, and a Session absent from input.sessions returns undefined, which session-setting-intent.ts:166-169 reads as "the snapshot has not caught up" and pins the overlay. clearSessionRendererState (app-shell.tsx:977-984) covers the removal path today, so this stays a shape risk rather than a live defect. Distinguishing "no longer in the catalog" from "revision not reached" would close it.
P3: the Plan chip keeps the retirement rule this PR just showed to be wrong. The reason is in the body and in the code comment, which is what I asked for. Please open a follow-up issue for the abandonPlanProposal contract change so the difference outlives this PR description.
Evidence boundary: read only at 6cc66318, no build, no tests run, and I did not run the app. CI is green at this head.
|
|
||
| export type SessionSettingIntentWriteResult = | ||
| | boolean | ||
| | SessionSettingIntentRevisionWriteResult; |
There was a problem hiding this comment.
P3: nothing in the repo consumes SessionSettingIntentWriteResult any more. The only reference is the test at session-setting-intent.test.ts:117-126, which exists solely to assert the alias still exists, so the compatibility it preserves has no client. The discriminated union above is the contract now. Deleting the alias and that test turns this PR from a net addition into one that removes a concept, which is the outcome I was after in the original note.
| }, | ||
| planMode: { | ||
| // Exiting a pending proposal returns Plan state rather than a Session | ||
| // summary, so this policy channel has no authoritative Session revision. |
There was a problem hiding this comment.
P3: this comment is the right place for the reason, and it is accurate (commitPlanMode leaves Plan through abandonPlanProposal, which returns PlanSessionState). Please also open a follow-up issue for that contract change and reference it here, so the last channel on the old retirement rule is tracked somewhere other than this PR's description.
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 apache#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 Generated-by: GLM-5.3-Flash (ZCode)
Summary
This is a focused follow-up to #3749. It addresses the review note that optimistic Session-setting overlays could retire on an unrelated renderer-wide catalog revision before the target Session snapshot had observed the committed write.
Keep the optimistic model, thinking-level, permission, and orchestration overlays until the target Session's catalog revision reaches the revision returned by the mutation. Preserve that authoritative revision through the Desktop projection and make revision-returning intent channels require a matching Session-revision observer.
Plan remains on catalog-wide retirement because
commitPlanModeis a policy flow: its pending-proposal path completes throughabandonPlanProposal, which returnsPlanSessionStaterather than a Session revision. Migrating that path requires a separate contract change.This does not change the Runtime Host wire protocol or persistence format.
Verification
npm --workspace @maka/ui run buildnode --test packages/ui/dist/session-setting-intent.test.js(5 passed)npm --workspace @maka/desktop run build:mainnode --test apps/desktop/dist/main/__tests__/session-settings-controller.test.js(7 passed)npm run format:checknpm run lintnpm run typechecknpm run check:renderer-architecture(61 passed)npm --workspace @maka/runtime-host run test:dist(1567 passed, 12 skipped)The root parallel
npm testpassed once before the final compatibility-only type-alias amendment. The final run completed with 1566 Runtime Host tests passing, 12 skipped, and one pre-existingpeer-meshroute-reconciliation timeout; that exact failing test passed immediately when rerun alone (1 passed). All other workspaces passed in the final run.AI use
Select exactly one:
Tool(s) and scope: Codex implemented revision propagation and causal overlay retirement for model, thinking-level, permission, and orchestration settings; added regression and type-contract tests; and assisted with review. All affected commits include
Generated-by: Codextrailers.Checklist
Does this PR entail a change in behavior?