← the Atlas

Agent Attention — One Model, One Place

Analysis·budding·implemented·

When an agent finishes or asks a question, kolu can play a sound, show a popup, badge the app icon, and mark the dock and host tab. Today those rules live in three modules split by which host you're looking at, so the same terminal behaves differently depending on focus. This note picks one word for it — attention — writes down the rules kolu should follow, and puts them in one place.

When an agent finishes a turn, or stops to ask you a question, kolu can react in several ways: play a sound, show an OS popup, badge the app icon, put a mark on the host tab, and color the dot in the dock. All of that is one thing: getting your attention.

The reactions themselves are fine. The problem is where the rules live. They’re spread across three modules, split by one thing that shouldn’t matter: whether you’re currently looking at that host.

So a finished agent on a background host tells you nothing, while the same agent on the host you’re looking at plays a sound. That split is why the whole thing is hard to reason about.

Now — split by which host you're looking at useTerminalAlerts active host only sound · popup · unread · "finished" useHostAttention other hosts only popup · app badge · "asking" ONLY unread ledger active host only Same terminal, different rules depending on focus. A finish on another host tells you nothing. tidy up Target — one owner, same rules everywhere every host's agent states (one shared fact) useAttention one place for the rules; "seen?" is the only host check attentionNotify (delivery, unchanged) sound + popup host-tab marks dock dots app icon badge One set of rules feeds every screen cue, on every host.
Today three modules hold the rules, split by whether you're looking at the host. The fix is one module that applies the same rules everywhere, sitting on top of the existing delivery code.

One word: attention

The code calls this three different things — alerts, attention, notifications — which is half the confusion. Pick one: attention (the system that decides when a terminal needs you, and how to show it). The renames that follow:

Keep the word notification for one thing only: the OS/browser popup — that’s the platform’s own word (the Notification API). It’s a channel attention uses, not the concept.

What you should experience

There should be one set of rules, the same on every host. Whether you’re looking at a host only changes one thing: whether the terminal counts as already seen.

Agent state Sound + popup (once) On-screen marks Goes away when
Asking you (waiting for input) if not already seen violet count pill · violet-washed row + tab · full-strength glowing pip · a chip counting how long it has waited you answer — nothing else
Just finished (no input needed) if not already seen amber count pill · amber-washed row + tab · amber pip badge you look at it
Working (or settling, or a shell that is printing) a plain rust ACTIVE count + spinner it stops moving
Idle

The counts share one shape — identical geometry — and split by hue, not by intensity: violet for asking (blocked on you, a state) and amber for an unseen finish (an unopened obligation). Hue carries the meaning so neither mark has to be the quiet one.The amber count reads the cross-host unseen-finished mark useAttention publishes, not the raw finishedIds — a finished agent idles in waiting ~forever, so “has any finished agent” would light every host permanently. The unseen rule is the pure, unit-tested nextUnseenFinished.

Two rules keep the vocabulary honest, and both are about what a mark promises:

Both surfaces put the mark on the whole row, not a dot on it (#1990). Unseen-finished washes the entire host tab amber; an unread dock row washes amber and fills its 3px leading stripe. The earlier design hung a 6px dot off the tab and a 7px corner badge off the dock pip — and the dock badge already pulsed, which is the useful evidence: unread threads still went unnoticed for hours. Motion on a 7px object doesn’t buy attention; area does. Doubling a dot’s diameter is a 4× area change, while tinting the surface behind it is ~100×. The pip’s corner badge stays as the precise mark — it just isn’t asked to do the catching.

The only place “which host” enters is seen: a terminal you’re staring at is seen by definition; one on a background host isn’t. Everything else is identical across hosts.

Why it’s tangled today

Two parts of this are already clean and should stay:

The tangle is the middle — the rules (fire once, only if unseen, roll up per host, badge the app). That middle is what got split three ways by the active-host line. Nobody planned that split; it grew.

The fix — one owner

Pull the three rule-fragments into a single useAttention module that:

  1. reads every host’s agent states (the shared fact the host tabs already use),
  2. keeps one “seen” record across all hosts (not just the active one),
  3. emits what each screen cue needs — the per-host marks, the dock dots, the app badge, and a “new, unseen” signal for the sound/popup.

Then useTerminalAlerts and useHostAttention disappear into it. The active-vs-other split is gone, because “seen” becomes a simple per-terminal check instead of two separate code paths.

Built

Shipped in #1931. The three rule-fragments now live in one packages/client/src/attention/useAttention.ts, wired once in App.tsx; useTerminalAlerts, useHostAttention, and the old activity-alert delivery module are gone. Every host runs the same rules off its urgency cell (now carrying finishedIds too), so — falling out for free — a background host’s finished agent now pings and shows a quiet host-tab mark, exactly like the active host’s. The activityAlerts setting was renamed attentionAlerts (with a migration), and the “needs you now / just finished” decision is the pure, unit-tested attentionTransitions.

One vocabulary, every altitude

Shipping the one owner did not give the one vocabulary. A year of surfaces had each hand-rolled its own presentation of the same facts, and the result was a 20-hour miss: an agent sat blocked on a question behind a needs-you pip drawn at 55% alpha, six repo headers wearing pill-shaped row-tallies that looked exactly like notification badges, and a violet host-tab count that could not be clicked. The real signal rendered quieter than the decoys.

The cause was structural, so the fix is too — one fold, one component, and the old spellings deleted so drift cannot recur (#2019):

Honest “finished” (EF2)

finishedIds is effective quiet, sticky per waiting episode: active + agent waiting + first quiet-crossing (~5s with no meaningful output), then held until the agent leaves waiting. Background sub-agents still typing hold the finished chime; idle TUI statusline ticks after finish do not un-finish and re-chime. Live dots and finish both ride kaval’s resize-aware activity edge — track EF in Effective Finish — Quiet Chronology, Phased (#1945 EF1, #1947 EF2).