Session status model — final structure (5 states: Working / Needs input / Ready / Stalled / Idle) #332
harshitsinghbhandari
started this conversation in
Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Final structure for the session status model, following the decisions on #331 and the review that followed. This is the spec to build against. Reply with changes and I will fold them in.
Background and the original problem statement are in #331 (three parallel mappings off
SessionStatus,no_signalcomputed then discarded, "Needs input" overloaded, duplicated pill tables). This post is the resolved design.Why we are doing this
The dashboard exists to do one thing well: let you run many agents in parallel and tell, at a glance, which ones need you. Every design choice should serve that scan. The current status model works against it.
A status is only worth showing if it changes what you do. Across everything we render there are really only a handful of distinct moves when you are scanning a wall of agents:
That is five moves. Today we expand one raw signal into thirteen
SessionStatusvalues and render them through three different groupings, and most of those thirteen map to the same move, so they add states to scan without changing the decision you make. That is cognitive load with no payoff. The fine PR detail (CI failing vs changes requested vs approved) still matters, but it belongs in the inspector where you are already looking at that one PR, not in the glanceable pill where it is noise.So we derive the states from the distinct moves, not from the backend values. Five states, each tied to one clear "what do I do."
We are not deleting information. The backend still has the full PR facts and activity history; this is purely about how much the glance surfaces. Collapsing is choosing the right altitude per surface: the pill answers "what does this session need from me right now," the inspector answers "what exactly is going on."
The five states
The change from the earlier draft of this spec is that the old
Readybucket was doing two jobs (a clean one-click PR and an agent that quit on an unfinished PR), and the oldStuckonly caught "never booted." Both are fixed below.How we cut: whose move is it
The clean cut is not "is there a PR." It is two axes: what the agent is doing, and what the PR needs. Every case is a cell, and the five states fall straight out.
PR buckets:
mergeable,approved,review_required. There is a real, clean action for you.draft,ci_failed,changes_requested, merge-conflicting. The agent has more to do.pr_open(open, no review requested, not mergeable, CI not failing). Just sitting there.***active-wins blind spot, see below. Barepr_openbehaves as "no PR" for status purposes: stopped on a neutral PR reads Idle.Two things this grid makes explicit. First, an agent that stopped on an unfinished PR (failing CI, requested changes, still a draft) is Stalled, not Ready. It had the move and quit; folding it into a green "Ready" tells you to act, you open it, and there is nothing to do but kick the agent. That is the exact trust erosion this whole effort is trying to kill. Second, the entire silent row is one move regardless of PR: the agent is not responding, go look.
The single derivation
One function, evaluated top to bottom, first match wins. Every surface reads from this and nothing re-derives. Inputs:
activity.state,isTerminated, the PR buckets above, and three timing facts (FirstSignalAt,LastActivityAt,signalCapable).where:
Notes:
stalled()is checked before theactivebranch on purpose, so a hung-but-still-"active" session is caught instead of reading Working.stalled()is checked before the clean-PR branch, so the silent row outranks "go review." Reasonable (a possibly-broken agent is more urgent than a review), and easy to flip if we disagree later.BOOT_GRACEis the oldnoSignalGrace, raised 90s -> 120s.HANG_TIMEOUTis the one genuinely open number (see Open items).Where this lives (important implementation note)
This mapping cannot be a remapping of the current 13-value
SessionStatus. That value already resolved the active-vs-PR precedence the wrong way for this model (PR wins), so once a session readsci_failedyou have lost whether the agent is active, which is exactly the bit we now need. The new state must be derived fromactivity.stateplus the raw PR buckets, not from the pre-collapsed status.Recommendation: move the single source of truth into the backend by rewriting
service/session/deriveStatusto emit these five states directly. It already has the activity state, the PR facts, the clock, andFirstSignalAt, so the timing logic (BOOT_GRACE,HANG_TIMEOUT) lives in one place with the data instead of being reimplemented in TypeScript. The frontend then renders the state verbatim and deletesworkerDisplayStatus,attentionZone,sessionNeedsAttention, andworkerStatusPulsesentirely. One function, server-side, every surface downstream of it.Current vs proposed
Today: one raw signal expands into 13
SessionStatusvalues, run through three independent groupings (workerDisplayStatusfor the pill,attentionZonefor the board, plus thesessionNeedsAttention/workerStatusPulsespredicates), rendered across four surfaces with the pill table duplicated in two of them. The pill shows six labels (Working, Needs input, CI failed, Ready, Done, Idle), the board shows five columns, and they do not line up.no_signalis computed by the backend but has no frontend state, so it falls back to "Working".Proposed:
activity.stateplus PR facts go through one backend function to five states (Working, Needs input, Ready, Stalled, Idle), and every surface renders that state verbatim.Per case, how it renders now versus what it becomes (bold = change). The proposed column splits on agent activity where it matters, because active now wins:
waiting_inputno_signal)HANG_TIMEOUTci_failedchanges_requesteddraftreview_requiredapprovedmergeablepr_openmergedterminatedidle)Today the pill and board disagree on several rows, and three states (
no_signal,idle, activeworking) all collapse to "Working" so a possibly-broken agent is invisible. The proposed column has one value per case, rendered identically everywhere.Decisions resolved (from the #331 review)
StalledreplacesStuck; the ceiling stays at five. "The agent will not finish on its own" is one move whether the cause is never-booted, hung mid-run, or stopped-with-unfinished-work. Restart-vs-reprompt is a real difference, but it is one you learn when you open the session, not on the wall. So slot four is redefined and widened rather than split into a sixth state.no_signaldetector is widened. "Never booted" is just the boot-time special case of "not making progress." The mid-run hang (agent worked, then wedged on a tool call or rate limit) is the failure that actually costs you, and today it shows a calm breathing "Working." We accept the false positives that a time-based hang detector brings, because missing the hang is worse.Readyis tightened to clean PRs only. mergeable, approved, review-required. Unfinished PRs leaveReady: Working while the agent is active, Stalled once it stops.Draftreference in the backend is read-only observation from GitHub viaobserve/scm/observer.go; there is no PR-creation path and no draft-as-handoff convention). So a draft is just an unfinished, merge-blocked PR and folds in withci_failed/changes_requested: Working while active, Stalled when stopped.Needs input+Stalled. Those are the states that actually need a human to unblock progress.Readyis a softer "your move" that should not nag.Open items
HANG_TIMEOUT. The seconds of active-but-silent before a session flips to Stalled. Too low and long legitimate turns false-flag; too high and we eat the hang. This wants a real number from watching live sessions, not a guess. Marked TBD.pr_openwhen stopped. Weak lean is Idle (PR is up, nothing broken, nothing requested of you). Flip to Ready if we would rather nudge on any open PR.Stalledcolor. Deferred. Label is "Stalled". It should not breathe (it is not alive); a muted warning tone fits its "go look" move.Per-surface rendering
Workingbreathes; the rest are static.attentionZoneset). Proposed left-to-right by urgency:Needs input,Stalled,Ready,Working,Idle.Idlerenders distinctly, not folded intoWorking.Needs inputandStalledonly.Out of scope (unchanged)
ActivityStatederivation (claudecode/activity.goand siblings).exited.NotificationNeedsInputalready fires only on the transition intowaiting_input, consistent with "Needs input = agent blocked".ready_to_merge/pr_merged/pr_closed_unmergeduntouched.POST /activityvalidation and the persistedactivity_stateCHECK set stay at the fourActivityStatevalues. The new states need no API enum change: the wirestatusfield is already a plain string. If we keep any client-side fallback, the frontendSessionStatusunion should learn the new vocabulary so nothing silently maps to "working".Implementation blast radius
backend/internal/service/session/status.go— the rewrite lands here:deriveStatusemits the five states fromactivity.state+ PR buckets + timing; add theHANG_TIMEOUTdetector and theBOOT_GRACE90s -> 120s bump; tighten the PR buckets (clean vs unfinished vs neutral).backend/internal/domain/status.go— the status vocabulary the API serializes (five states, or keep the richer set internally and project to five at the edge).frontend/src/renderer/types/workspace.ts— deleteworkerDisplayStatus,attentionZone,sessionNeedsAttention,workerStatusPulses; render the backend state verbatim; teachSessionStatusthe new vocabulary.frontend/src/renderer/components/SessionsBoard.tsx—COLUMNS,BADGE.frontend/src/renderer/components/ShellTopbar.tsx—STATUS_PILL.frontend/src/renderer/components/SessionInspector.tsx—STATUS_PILL,activityDetail, theidlespecial-case.frontend/src/renderer/components/Sidebar.tsx—SessionDot.backend/internal/service/session/status_test.goandfrontend/src/renderer/types/workspace.test.ts.Beta Was this translation helpful? Give feedback.
All reactions