Handler: confirm the undo that leaves this machine, band escalations, answer the judge question before arming - #69
Merged
Conversation
Three of the four snapshot actions undo locally; undoing a force push writes to a shared remote. The offer is a scrolling list row whose entire body is the tap target, newest first, so the row most likely to sit under a thumb was also the one that could overwrite a ref for everyone on it. Confirmed for force_push alone. The other three keep the one-tap prevention §5.2 buys back, and the dialog promises no recovery: the bridge pins the remote tip before overwriting it only when the ref still exists there.
urgency reached exactly one consumer, the OS notification title, and that path returns early while you are viewing the session it names. Inside the app the field was invisible: high and normal rendered identically, and oldest-first sorting filed a fresh urgent row last. high is not judge opinion. The engine mints it with no judge call at all for a blocking prompt, so it means the agent is stopped right now. Banded ordering with age deciding within a band, applied on the live push too, and marked in the meta column the three escalation row shapes share.
The catalog already carried judgeCapable and the app already resolved the agent the way the bridge does, so the pre-arm half of the coverage answer was one field away. Until now it surfaced only as an ESCALATE ONLY chip, found on returning to a session that had woken you for everything. The prediction is exact rather than approximate because nothing writes a per-session judge override yet: observabilityFor falls back to the session's own tool, which is what the catalog describes. A judge picker inherits the job of keeping that true. Withheld from the unwatchable arm, which already carries the stronger fact.
latestEscalationId read escalations.last, which the new banding turned into the newest NORMAL escalation — never an urgent one, i.e. never the row a caller asking for the latest wants. Folded on at instead. No live caller today, so this was a trap rather than a bug. The undo tap started async work from a void callback with a bare unawaited, which app/CLAUDE.md forbids outright: a throw past the dialog reaches PlatformDispatcher.onError as a FATAL with no in-app frames. Uses detached now. The urgency test is a shared escalationMeta closure rather than three hand-written copies, which is what the adjacent comment already claimed. compareEscalations' doc asserted high is engine-minted only. It is not: escalate passes the judge's own notify.urgency through, so a judge-authored high sorts into the same band. The band is still right; the claim was not. Two bridge comments calling backlog.ts import-free were wrong the same way — it imports zod and ./evidence; what it actually has is a position below every consumer.
The escalate-only caveat added to the arm explainer almost never rendered: the explainer is gated on FirstRunState.handlerArmedOnce, a once-EVER latch, while coverage is per-agent. A user whose first arm was a judge-capable agent never sees that dialog again and would meet an escalate-only one with no warning at all. The shield tooltip is the pre-arm surface that answers every time, and it already carried the observability half. handlerShieldTooltip is top-level for the reason handlerArmExplainerBody is — the precedence is testable without pumping the panel — and keeps the explainer's order: unwatchable outranks escalate-only, since a session reporting nothing makes its judge moot.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three Handler follow-ups from the HF review, each with its own tests.
HF-10 — the one undo that leaves this machine asks first
Snapshot undo has four actions.
reset_hard,rm_rfandgit_cleanrestore this checkout and nobody else's;force_pushwrites to a shared remote. The offer sits on a scrolling list row whose whole body is the tap target, so a thumb landing where the scroll stopped could overwrite a ref for everyone on it.force_pushnow goes throughAbConfirmDialog, naming the entry so the ref being overwritten is on screen. The other three stay one tap — the confirm is bought by blast radius, not by the word "undo". No promise of recovery in the copy: the bridge pins the current remote tip before overwriting, but only when the ref still exists there (snapshot.ts).HF-5 — an escalation that stopped the agent sorts above one that merely waited
urgencywas on the wire and observable only as an OS notification for a session you are not looking at (workspace_shell.dart:475early-returns via_isViewingSession). Meanwhile the flat oldest-first sort filed a fresh blocking prompt under every stale question already on the list.New
compareEscalationsinhandler_state.dart: urgent band first, oldest-first within each band, age never crossing the band. An urgency a newer bridge invents ranks asnormal— the safe band, since it claims nothing. Applied at both the status-replay sort and the live push (sorted on the way in: the push raises the toast, and the row the user came to answer would otherwise be at the bottom of the list they were sent to).An
URGENTchip rides_RowMeta, the only widget all three escalation row shapes share, so it cannot be added to two and forgotten on the third.HF-7 — escalate-only is answerable before arming, not after walking away
ESCALATE ONLYwas a post-arm chip you find by walking away and coming back to a session that woke you for everything.FocusedSessionCoveragenow carriesjudgeCapable, and both pre-arm surfaces state it:agent_panel.dart— the surface that answers every time. The explainer sits behindfirstRunProvider.handlerArmedOnce, a once-ever global latch, while coverage is per-agent: a user whose first arm was a capable agent would meet an escalate-only one with no warning at all.Precedence extracted as top-level
handlerShieldTooltipso it is unit-testable without pumping the panel.Review corrections
Three defects in the above, found by
/code-reviewand each verified before accepting:unawaited, violatingapp/CLAUDE.md:41— a discarded future's rejection reachesPlatformDispatcher.onErroras a FATAL with no in-app frames. Nowdetached(...).latestEscalationIdreadescalations.last, which the new banding silently turned into "newest normal escalation, never an urgent one". Now folds onat.compareEscalationsdoc claim that overstated wherehighcomes from: the engine mints it itself for blocking prompts with no judge call, butescalatealso passes the judge's ownnotify.urgencythrough, so a judge can authorhighon a routine question. The banding stands; the justification was too strong.Gates
flutter analyze— No issues foundflutter test— 3026 passed, 2 skippednpm run check:font-tokens— OKbridge/diff is comment-only, so no bridge runIndependent of #68 — both branch off
developmentand touch disjoint regions ofhandler_service.dart.