Skip to content

Handler: confirm the undo that leaves this machine, band escalations, answer the judge question before arming - #69

Merged
bharathm03 merged 5 commits into
developmentfrom
antgrid/handler-hf-5-7-10
Sep 1, 2026
Merged

Handler: confirm the undo that leaves this machine, band escalations, answer the judge question before arming#69
bharathm03 merged 5 commits into
developmentfrom
antgrid/handler-hf-5-7-10

Conversation

@bharathm03

Copy link
Copy Markdown
Contributor

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_rf and git_clean restore this checkout and nobody else's; force_push writes 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_push now goes through AbConfirmDialog, 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

urgency was on the wire and observable only as an OS notification for a session you are not looking at (workspace_shell.dart:475 early-returns via _isViewingSession). Meanwhile the flat oldest-first sort filed a fresh blocking prompt under every stale question already on the list.

New compareEscalations in handler_state.dart: urgent band first, oldest-first within each band, age never crossing the band. An urgency a newer bridge invents ranks as normal — 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 URGENT chip 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 ONLY was a post-arm chip you find by walking away and coming back to a session that woke you for everything. FocusedSessionCoverage now carries judgeCapable, and both pre-arm surfaces state it:

  • the arm explainer body, on the watchable arm only (an unwatchable agent reports nothing, so what its judge could have done is moot, and a hedge under the stronger fact dilutes it)
  • the shield tooltip in agent_panel.dart — the surface that answers every time. The explainer sits behind firstRunProvider.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 handlerShieldTooltip so it is unit-testable without pumping the panel.

Review corrections

Three defects in the above, found by /code-review and each verified before accepting:

  • the undo callback used unawaited, violating app/CLAUDE.md:41 — a discarded future's rejection reaches PlatformDispatcher.onError as a FATAL with no in-app frames. Now detached(...).
  • latestEscalationId read escalations.last, which the new banding silently turned into "newest normal escalation, never an urgent one". Now folds on at.
  • a compareEscalations doc claim that overstated where high comes from: the engine mints it itself for blocking prompts with no judge call, but escalate also passes the judge's own notify.urgency through, so a judge can author high on a routine question. The banding stands; the justification was too strong.

Gates

  • flutter analyze — No issues found
  • flutter test — 3026 passed, 2 skipped
  • npm run check:font-tokens — OK
  • bridge/ diff is comment-only, so no bridge run

Independent of #68 — both branch off development and touch disjoint regions of handler_service.dart.

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.
@bharathm03
bharathm03 merged commit 101d180 into development Sep 1, 2026
4 checks passed
@bharathm03
bharathm03 deleted the antgrid/handler-hf-5-7-10 branch September 1, 2026 09:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant