Skip to content

feat(web): one cross-session "Needs you" queue - #330

Merged
saucam merged 2 commits into
mainfrom
feat/p5-3-attention-inbox
Sep 12, 2026
Merged

feat(web): one cross-session "Needs you" queue#330
saucam merged 2 commits into
mainfrom
feat/p5-3-attention-inbox

Conversation

@saucam

@saucam saucam commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

P5.3 (conductor-frontends-design.md §8). Every blocking event across every agent in one ranked list, with the count always visible in the status bar and the queue one click away.

The problem it removes

Today you find the agent that stopped by opening sessions until you hit it. Three unrelated things block — a session wedged on a tool approval, a provider dialog waiting on an answer, a dispatch task that hit the anti-spin limit — and none of them announces itself anywhere shared.

Ranking — §8's (blocking-cost × staleness)

A question or approval has stopped a live agent mid-turn, so it outranks a blocked task, which is stopped but burning nothing. failed ranks lowest — it's the one kind that can still resolve without you, since it retries with backoff (§6).

Staleness then overtakes kind as things sit, which is what makes §8's worked example true: "an agent stuck 8 minutes on a yes/no floats to the top."

An age floor is what makes that work at both ends. Without it every fresh item scores zero regardless of kind, so the queue would rank purely by age exactly when a live wedged agent most needs to be first.

Three correctness details

One stoppage is one row. A session showing a provider dialog is usually also waiting_approval. Counting both would tell you two things need you when one does — in a queue whose entire job is that count.

An unknown age reads as brand new, never ancient, so something with no usable timestamp can't jump the queue. Future timestamps clamp to zero age too: clock skew between daemon and browser is normal and mustn't bury a real interruption.

Ties break totally and stably (since, then key). A queue that reshuffles equal rows between renders is one you can't click accurately.

An honest approximation

Neither a ui_request nor a FleetTaskWire carries the moment it began waiting. A session falls back to lastActivityAt (a wedged session stops producing activity the instant it wedges); a task falls back to its creation time. Both err toward looking older than reality, which biases toward surfacing rather than hiding — the right direction for this list. The code says so where it matters.

One small UX call

The badge shows zero rather than disappearing. A badge that vanishes can't be trusted as an at-a-glance "nothing is stuck" signal, because you can't tell clear from broken.

Verified against a real board

Not just fixtures — running the queue over a captured board plus a simulated 3-minute-old wedged agent:

queue length: 3

  [blocked ] spawn c0234348    reclaimed: stale claim
  [blocked ] spawn cff8e6c5    reclaimed: stale claim
  [approval] api               waiting for tool approval

The two tasks blocked by the old daemon-collision bug (#319) outrank the fresher wedged agent — staleness overtaking kind weight, exactly as specified — and the finished tasks stay out.

Verification: 14 attention tests, 243 web tests total, typecheck / lint / build clean.

Next

§8's remaining pieces are the four-action approval grammar (Approve · Edit args · Respond · Reject) and approval mirroring with escalate-to-parent — both of which extend ApprovalBar rather than this queue.

🤖 Generated with Claude Code

saucam and others added 2 commits September 7, 2026 13:16
P5.3 (docs/conductor-frontends-design.md §8). Every blocking event across
every agent in one ranked list, with the count always visible in the
status bar and the queue one click away.

The problem it removes: today you find the agent that stopped by opening
sessions until you hit it. Three unrelated things block — a session
wedged on a tool approval, a provider dialog waiting on an answer, a
dispatch task that hit the anti-spin limit — and none of them announces
itself anywhere shared.

Collection and ranking are pure (`lib/attention.ts`); `state/attention.ts`
only joins the three sources; the component is the surface.

**Ranked by §8's (blocking-cost × staleness).** A `question` or
`approval` has stopped a LIVE agent mid-turn, so it outranks a `blocked`
task, which is stopped but burning nothing. `failed` ranks lowest — it is
the one kind that can still resolve without you, since it retries with
backoff. Staleness then overtakes kind as things sit, which is what makes
"an agent stuck 8 minutes on a yes/no floats to the top" true.

An age FLOOR is what makes that work at both ends. Without it every fresh
item scores zero regardless of kind, so the queue would rank purely by
age exactly when a live wedged agent most needs to be first.

Three correctness details worth naming:

- **One stoppage is one row.** A session showing a provider dialog is
  usually ALSO `waiting_approval`; counting both would tell you two
  things need you when one does, in a queue whose entire job is that
  count.
- **An unknown age reads as brand new, never ancient**, so something with
  no usable timestamp cannot jump the queue. Future timestamps clamp to
  zero age too — clock skew between daemon and browser is normal and must
  not bury a real interruption.
- **Ties break totally and stably** (since, then key). A queue that
  reshuffles equal rows between renders is one you cannot click.

Timestamps are approximate and the code says so: neither a ui_request nor
a FleetTaskWire carries the moment it began waiting, so a session falls
back to `lastActivityAt` (a wedged session stops producing activity the
instant it wedges) and a task to its creation time. Both err toward
looking OLDER than reality, which biases toward surfacing rather than
hiding — the right direction here.

The badge shows zero rather than disappearing: a badge that vanishes
cannot be trusted as an at-a-glance "nothing is stuck" signal, because
you cannot tell clear from broken.

Verified against a real board, not just fixtures: the two tasks blocked
by the old daemon-collision bug outrank a 3-minute-old wedged agent —
staleness overtaking kind weight — and the finished tasks stay out.

14 attention tests; 243 web tests; typecheck, lint and build clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@saucam
saucam merged commit 96c93da into main Sep 12, 2026
4 checks passed
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.

2 participants