← the Atlas

Effective Finish — Quiet Chronology, Phased

Analysis·seedling·proposed·

Plan of record for honest “agent finished” attention — kaval owns resize-aware meaningful-output chronology; activity cutover (EF1) and pure-fold finishedIds via a second createActivityTracker (EF2) shipped. Phased EF0–EF3 (EF0–EF2 done) after closing the

An agent (Claude Code is the motivating case) can mark its turn waiting while background sub-agents still emit bytes. Raw waiting in finishedIds is then a premature finish — chime and host-tab mark before the work actually stopped. The fix is not a second byte-tap state machine in padi. It is:

  1. kaval reports meaningful PTY output (resize repaints excluded at the source),
  2. padi + client cut over live-dots onto that fact in one PR (no dual path) — done,
  3. padi pure-folds finish from the same edge via a second createActivityTracker at a multi-second window (EF2) — done.

Work items use the EF track prefix (new; no Atlas collisions). #1938 closed without merge — design kept, big-bang delivery discarded. This note is the plan of record (#1944). EF1 shipped (#1945); EF2 shipped (#1947): finishedIds is effective quiet, not raw waiting.

EF — phase tree (each box is one mergeable PR) EF0–EF2 done. EF3 is shared fan-out / e2e / docs polish. Never ship a dead wire beside the old mechanism. EF0 · close / capture ✓ done · #1938 closed · plan #1944 EF1 · activity cutover ✓ shipped #1945 · kaval → padi → client no dual path · resize mute at source live dots on host-global fact EF2 · pure finish fold ✓ shipped #1947 · 2nd tracker @ ~5s daemon-lifetime · dual-edge urgency sticky-per-episode finished EF3 · harden / e2e / docs shared fan-out · visit regression · polish Shipped stack (EF1 + EF2) kaval · meaningful-output edge onData · resize mute · throttle · contract bump padi · activity live set (EF1) fold edge through short idle window · drop byte taps client · isLive mirror (EF1) plain array copies · drop suppress / noteOutput · live→idle test EF2 · finish tracker ✓ #1947 createActivityTracker(EFFECTIVE_FINISH_QUIET_MS) noteOutput: enter-waiting · kaval edge · boot seed · resubscribe restamp finished = waiting ∧ sticky quiet · dual-edge · no idle re-chime
EF phase tree (left) and stack (right). EF0–EF2 done (dashed). EF3 is shared fan-out / e2e / docs polish.

The problem

Symptom Cause
Premature “finished” finishedIds = raw agent waiting packages/padi/src/activity/urgency.ts
Visit finished tile → re-chime First #1938 design made finish byte-reactive; client resize mute did not travel to padi taps
Sticky “all terminals live” #1938 Phase D stored a Solid reconcile proxy as prev; removals never applied

Attention already has one model (docs/atlas/src/content/atlas/agent-attention.mdx / #1931). This note only makes the finished half of that model honest — and, as a prerequisite, makes activity a single source of truth.

End state

Meaningful output (EF1 — shipped)

Fact Writer
Meaningful-output edge { id } kaval onData (resize-muted bursts excluded)

Consumers stamp arrival on their clock (recency-free — nothing cross-host compares two hosts’ clocks). Never ship kaval absolute timestamps for finish.

Two windows, one tracker type (EF1 + EF2)

EF1 extracted packages/padi/src/activity/terminalActivityTracker.ts and wired it for live-dots. Its header already reserves isLive / forget for the finish consumer. EF2 does not re-derive stamps + a hand deadline — it builds a second createActivityTracker(EFFECTIVE_FINISH_QUIET_MS) instance.

Consumer Window Read Lifetime
Live dots (liveActivity + client) TERMINAL_IDLE_AFTER_MS (~1s) snapshot() → stream Per activity-stream subscriber
Effective finish EFFECTIVE_FINISH_QUIET_MS (~5s) isLive(id) Daemon-lifetime (urgency honest with zero watchers)

Pure finish fold (mental model ≡ episode map + tracker)

episode: Map<id, "debouncing" | "finished">

finished(id) =
  active ∧ agentBucket = waiting
  ∧ episode.get(id) === "finished"

// events (not reads):
enter-waiting     → episode.set(id, "debouncing"); tracker.noteOutput(id)
quiet-exit        → episode.set(id, "finished")   // tracker.onChange
leave-waiting     → episode.delete(id); tracker.forget(id)
boot seed         → episode.set(id, "finished")   // discovery
edge / restamp    → tracker.noteOutput only if phase === "debouncing"

The first quiet-crossing is identical to the stamp form

now − max(enteredWaitingAt, lastArrival ?? enteredWaitingAt) ≥ QUIET

iff the tracker is fed for the first-finish debounce. Promotion to finished is an event (quiet exit), never a side-effect inside the predicate. Mid-waiting TUI noise does not re-arm a finished episode’s timer (and so does not dual-edge-recompute forever).

ASKING (awaiting_user) stays ungated — never gated on quiet.

EFFECTIVE_FINISH_QUIET_MS (~5s): long enough to ride gaps between background sub-agent output bursts; short enough the finished nudge isn’t noticeably late. Only the lesser finish path is debounced — never asking.

Default-excluded and recycle blindness

Graph wiring (urgency)

Today urgency is only terminals-driven:

// servePadi.ts today
urgency: derived.cell(($) => recomputeUrgency($.terminals())),

Quiet expiry is a non-event on terminals. EF2 must:

  1. Keep a daemon-lifetime finish tracker + standing kaval resubscribeStream (not nested inside liveActivity’s per-subscriber source).
  2. Publish the quiet/settled fact through a reactor source whose install is finishTracker.onChange (same pattern as hostInventory poll sources).
  3. Fold both edges:
urgency: derived.cell(($) =>
  recomputeUrgency($.terminals(), finishQuietFact()),
),

so timer expiry re-folds finishedIds without an agent-state change. Date.now() stays inside the tracker; recomputeUrgency stays pure over graph inputs.

Optional shared fan-out of the kaval activity stream (one sub, many consumers) can wait for EF3; the standing finish sub is EF2-required.

Sticky-per-episode (product decision — field-corrected)

Client attention treats “left both awaitingIds and finishedIds” as episode end and clears the fire-once latch (packages/client/src/attention/attentionTransitions.ts / packages/client/src/attention/attentionCore.ts). So if the server un-finishes while the agent stays waiting, the latch clears and a later re-quiet re-chimes.

Chosen semantics: sticky-per-episode. Once a waiting id crosses quiet and enters finishedIds, it stays finished until it leaves the waiting bucket (agent runs again / asks / leaves pool). Mid-waiting edges (including idle TUI statusline/clock/context ticks that pass kaval’s resize-only mute) do not un-finish.

Earlier plan chose re-chime-per-quiet-crossing under the assumption that idle terminals are byte-quiet. Field falsified that — every idle agent TUI that repaints on an interval > ~5s flapped finishedIds forever on every host. Sticky kills the flap for every noise source (including resubscribe restamp after the first quiet-crossing). The first-finish ~5s debounce is untouched.

Boot seed: already-waiting ids on first syncWaiting go straight to sticky finished (discovery is not a transition — matches the client baseline and pre-EF2 master; avoids mass chime on padi restart under a connected client).

Pin in unit tests:

Live activity (EF1 — shipped)

What kaval does not own

Quiet windows stay consumer policy. Kaval owns which bursts count (resize-aware filter + throttle), not a single live/quiet boolean with one internal timer.

Why not “edge only, consumers later”?

Shipping a kaval stream with nothing product-facing on it leaves the old mechanism in place. EF1 cut activity over completely. Finish is a different axis (attention policy) and stays EF2.

Phase tree

Strict dependency: EF0 → EF1 → EF2 → EF3. Each remaining box is one PR, green alone, revertible alone. EF0–EF2 are complete.

EF0 — close / capture

EF0· close / capture✓ shipped
blocks → EF1links #1938 closed · plan #1944

EF1 — activity cutover (kaval → padi → client)

EF1· activity cutover✓ shipped
needs ← EF0blocks → EF2links #1945 merged

Shipped in #1945 — one volatility, full stack:

Layer Shipped Deleted
kaval Host-global activity edges ({ id }); emit from onData; resize mute before proc.resize; throttle; pure shouldEmitActivityEdge + tests; contract 5.3
padi liveActivity folds the edge through the short idle tracker Per-terminal activity byte taps / dirty reconcile for taps
client useAttentionFacts mirrors per-host streams.activity; plain array copies for frame/prev noteOutput, suppress, RESIZE_ACTIVITY_SUPPRESS_MS, attach-sink lighting

Finish policy (finishedIds) was deliberately not changed here — that is EF2.

EF2 — pure finish fold (finish honesty)

EF2· pure finish fold✓ shipped
needs ← EF1blocks → EF3links #1947

Shipped in #1947 — tracker realization, not stamps + deadline; product corrected to sticky-per-episode after field flap:

Piece What
Tracker createActivityTracker(EFFECTIVE_FINISH_QUIET_MS) (~5s) — second instance of the EF1 leaf packages/padi/src/activity/terminalActivityTracker.ts via packages/padi/src/activity/finishQuiet.ts
Feed noteOutput on enter-waiting (awaitingwaiting, re-entry); every kaval edge (pre-finish debounce); resubscribe restamp of not-yet-sticky waiting ids
Boot already-waiting on first sync → sticky finished immediately (discovery)
Evict forget + sticky-clear on leave-waiting / leave-pool
Fold finishedIds = active waiting ∧ isEpisodeFinished(id) (sticky memoize) — packages/padi/src/activity/urgency.ts; asking ungated
Graph Daemon-lifetime standing kaval resubscribeStream + reactor push source on onChange; dual-edge derived.cell in packages/padi/src/servePadi.ts
Product Sticky-per-episode — once quiet-crossed, stay finished until leave waiting

Tests shipped

Case Layer
Enter waiting + quiet ≥ QUIET → in finishedIds fold / tracker
Edge mid-window resets; not finished until QUIET after last edge fold / tracker
Boot seed: already-waiting → immediately sticky-finished fold / tracker
Mid-waiting edge after finish does not un-finish fold / tracker
Resubscribe restamp pre-finish delays; post-sticky does not un-finish fold / tracker
Leave waiting / leave pool clears sticky; re-entry earns a fresh window fold / tracker
awaiting_userwaiting demotion starts window fold / tracker
Quiet timer expiry re-folds without a terminals write dual-edge integration
Steady finishedIds while waiting → no re-deliver; leave-both re-arms attention engine (observe)

Resize-class mute remains kaval EF1 (shouldEmitActivityEdge); no second mute in the finish fold.

EF3 — harden / e2e / docs

EF3· harden / e2e / docs○ todo
needs ← EF2

Anti-patterns

Do not Why
Dead kaval wire + old padi/client paths left in place Half-baked dual mechanism; untestable; second PR tax
One PR = activity cutover and finish fold Two volatilities; #1938 failure mode (bugs mask each other)
Quiet boolean with one window in kaval Complects producer with consumer policy
Absolute kaval time for the fold Breaks recency-free / invites cross-host compare
Hand-rolled stamp maps + deadline instead of second ActivityTracker Re-derives shipped EF1 machinery
Standing finish sub only while someone watches activity Urgency lies with zero stream subscribers
Assume “late only delays” without resubscribe restamp Lost edges during debounce → early finish
Assume idle terminals are byte-quiet (re-chime-per-quiet) Idle TUI statuslines flap finishedIds forever — field bug; sticky-per-episode is the plan of record
Strengthen kaval “meaningful” to kill idle ticks Heuristic whack-a-mole at the wrong layer; quiet policy is consumer-side
Delete client activity tests in the mirror cutover Sticky-live shipped silent
Call the pure fold a “gate v2” and claim deletion Reviewers cannot tell the machine is gone

Checklist per PR

Phase Must have
EF0 ✓ done — #1938 closed; plan #1944
EF1 ✓ done — #1945 activity cutover (kaval + padi + client; no dual path)
EF2 ✓ done — #1947 second tracker @ EFFECTIVE_FINISH_QUIET_MS; sticky-per-episode; boot-seed sticky; dual-edge urgency; fold + attention tests
EF3 shared fan-out and/or e2e; docs polish

Relation to attention

Agent Attention owns the rules (fire once, seen, host-tab, dock). This note owns (1) one activity fact end-to-end and (2) the truth of finished before those rules run. After EF1, live dots tell the truth about output; after EF2, finishedIds means effective quiet sticky for the waiting episode — first quiet-crossing sticks until leave-waiting.