fix(mode): a failed flush re-parks the pending intent instead of dropping it

Review hardening (the finding's ordering premise did not hold — see the
PR thread — but its failure-path kernel did): onBoundary cleared the
pending intent BEFORE appending the mode/set, so a backend rejecting
that one write lost the switch forever while the picker kept showing it
optimistically. The intent is now cleared only after the append lands;
a failed flush stays parked and the next healthy boundary converges the
log with the picker. The containment test extends to pin the re-park
and the retry.

The bridge's re-notify keeps deriving from the logged event's value —
now documented in place: the service holds ONE coalesced pending slot
(every flush reads the latest selection, so a stale flush cannot
exist), and for any other writer the logged value is the truth the
picker should track, in log order.
This commit is contained in:
kingwl
2026-07-10 16:28:11 +08:00
parent 61922af5cd
commit de9d618f0c
3 changed files with 22 additions and 2 deletions

View File

@@ -511,6 +511,11 @@ export function apply(ctx: Context, config: AcpConfig): void {
enabled: rec.terminalEnabled,
cwd: session.header.cwd,
}, { includeUserMessages: false })
// Re-notify from the EVENT's value, not from modes.get(): the service
// holds one coalesced pending slot (every flush reads the latest
// selection, so a flush can never be stale against the picker), and for
// any other writer — the exit tool, a test, a foreign plugin — the logged
// value IS the truth the picker should track, in log order.
if (event.type === 'mode/set' && event.data.mode !== rec.lastModeId) {
rec.lastModeId = event.data.mode
notify({ sessionId: rec.sessionId, update: { sessionUpdate: 'current_mode_update', currentModeId: event.data.mode } })