Summary
AO currently has the right raw ingredients for session state, but it combines and presents them with the wrong precedence:
session.activity Raw agent runtime activity
session.isTerminated AO/tmux session lifecycle fact
session.prs Durable SCM/PR facts
session.status Display status derived from the facts above
This causes a live agent to remain visually stuck in In review, Needs you, or Ready to merge after it resumes work. It also collapses an agent process exiting into the AO/tmux session being terminated, even though the tmux session can remain alive and inspectable.
The same underlying ambiguity reaches the UI:
- Kanban placement is driven by derived
session.status.
- The topbar activity pill is correctly driven by raw
session.activity.
- The sidebar dot is currently driven by Kanban/attention status instead of raw agent activity.
- Working and Needs you use adjacent orange/yellow colors that are difficult to distinguish.
- Merged, Terminated, Exited, and Unknown do not yet have a sufficiently clear and consistent semantic treatment.
This issue defines one canonical state-priority model, preserves the distinction between agent exit and tmux termination, aligns all affected frontend surfaces, and establishes a semantic color system for both dark and light themes.
User-facing problem
Active work can remain in the wrong Kanban column
Today, once a session owns an open PR, PR/SCM status is evaluated before activity.active. A session that resumes work can therefore remain:
- In review because the PR is open, draft, or awaiting review
- Needs you because CI failed or changes were requested
- Ready to merge because the PR is approved/mergeable
The board no longer answers the most immediate question: "Which agents are working right now?"
Expected behavior:
For every non-terminal session, active agent work wins. SCM determines board placement only when the agent is not actively working.
Agent exit and tmux termination are conflated
These are different facts:
activity = exited means the agent process/runtime inside the tmux session exited.
isTerminated = true means AO considers the managed session terminal.
A terminated tmux/AO session necessarily has no running agent, so termination may normalize activity to exited.
The reverse is not true. An agent can exit while tmux remains alive and leaves an interactive shell behind. That state must remain inspectable and visible as Exited in Needs you.
Status visuals do not communicate a stable semantic language
The current dark-theme tokens use:
Working #F59F4C orange
Needs you #E8C14A yellow/amber
These hues are too similar in compact dots, badges, and column accents. Merged and Terminated also collapse into passive Done styling, while Exited and Unknown are both muted gray in the activity presentation.
Current implementation and root cause
Backend display priority
backend/internal/service/session/status.go::deriveStatus() currently evaluates:
1. isTerminated -> merged or terminated
2. waiting_input / blocked -> needs_input
3. open PR aggregation
4. any merged PR
5. activity.active -> working
6. missing hook signal -> no_signal
7. idle
Because active comes after open PR and merged-PR checks, it cannot produce working while an open PR exists.
Lifecycle collapse
backend/internal/lifecycle/manager.go::ApplyActivitySignal() currently does this for ActivityExited:
if s.State == domain.ActivityExited {
next.IsTerminated = true
}
This destroys the valid combination:
activity = exited
isTerminated = false
tmux still alive
The storage model already persists activity_state and is_terminated independently, so no schema change is needed to represent this correctly.
Existing safety behavior
backend/internal/sessionguard/guard.go already refuses both user/API delivery and AO-generated nudges when activity = exited, even if isTerminated = false.
That check must remain. After the agent exits, the pane may contain an interactive shell; automatically pasting an agent prompt into it could execute the prompt as shell commands.
Important lifecycle caveat
In the current code, MarkTerminated updates durable state without necessarily tearing down the external runtime/worktree. Merge and tracker completion can call this flag-only path.
Physical runtime/worktree cleanup is already tracked separately in:
This issue must not duplicate that cleanup work. It should establish the state contract that an agent-exit hook alone never marks the AO session terminated. Any claim that isTerminated literally proves tmux destruction depends on the teardown issues above being resolved.
Canonical concepts
Agent activity
Raw, persisted activity reported by the agent hook pipeline:
active
idle
waiting_input
blocked
exited
Frontend-only unknown remains a defensive fallback for missing, invalid, or version-skewed data. It is not a backend business state.
Session status
A display status derived by the backend at read time from:
activity_state
is_terminated
- PR lifecycle, CI, review, comments, and mergeability facts
- hook-signal capability and first-signal timing
Derived status must not be stored.
Attention zone
The frontend grouping derived from backend session.status:
Working
Needs you
In review
Ready to merge
Done
Idle remains a nested stack at the bottom of Working, not a separate top-level column.
Required priority and board placement
| Priority |
AO/tmux session |
Agent activity |
SCM condition |
Derived status |
Kanban placement |
| 1 |
Terminated |
Exited |
Any attributed PR merged |
merged |
Done |
| 2 |
Terminated |
Exited |
No attributed PR merged |
terminated |
Done |
| 3 |
Alive |
Active |
Any non-terminal SCM condition |
working |
Working |
| 4 |
Alive |
Exited |
Any SCM condition |
exited |
Needs you |
| 5 |
Alive |
Waiting input |
Any SCM condition |
needs_input |
Needs you |
| 6 |
Alive |
Blocked |
Any SCM condition |
needs_input |
Needs you |
| 7 |
Alive |
Idle |
CI failing or changes requested |
Existing SCM status |
Needs you |
| 8 |
Alive |
Idle |
PR open, draft, or review pending |
Existing SCM status |
In review |
| 9 |
Alive |
Idle |
Approved or mergeable |
Existing SCM status |
Ready to merge |
| 10 |
Alive |
Idle |
No open PR |
idle |
Idle stack inside Working |
Additional defensive behavior:
no_signal remains in Needs you.
- Frontend
unknown remains visible in Needs you and should produce telemetry/logging.
- A merged PR observed before terminal bookkeeping completes may temporarily derive
merged as a defensive fallback, but activity.active still wins while the live agent is working.
Proposed backend derivation
if rec.IsTerminated {
if anyMerged(prs) {
return domain.StatusMerged
}
return domain.StatusTerminated
}
switch rec.Activity.State {
case domain.ActivityActive:
return domain.StatusWorking
case domain.ActivityExited:
return domain.StatusExited
case domain.ActivityWaitingInput, domain.ActivityBlocked:
return domain.StatusNeedsInput
}
open := openPRs(prs)
if len(open) > 0 {
return aggregatePRStatus(open)
}
if anyMerged(prs) {
return domain.StatusMerged
}
if signalCapable && rec.FirstSignalAt.IsZero() &&
now.Sub(rec.Activity.LastActivityAt) > noSignalGrace {
return domain.StatusNoSignal
}
return domain.StatusIdle
Transition examples
Agent resumes from In review
Before: activity=idle, PR=review_pending
Status: review_pending
Board: In review
Hook: activity -> active
After: status=working
Board: Working
Agent resumes from Needs you
Before: activity=waiting_input or blocked
Status: needs_input
Board: Needs you
Hook: activity -> active
After: status=working
Board: Working
The same applies when the session was in Needs you because CI failed or changes were requested: active work moves it to Working; once the agent returns to idle, SCM facts determine its column again.
Agent becomes idle after opening a PR
activity=idle
PR=open/draft/review_pending
Status: PR-derived status
Board: In review
Agent becomes idle without a PR
activity=idle
no open PR
Status: idle
Board: nested Idle stack inside Working
Agent exits but tmux remains
activity=exited
isTerminated=false
Status: exited
Board: Needs you
Terminal: remains inspectable
Automated pane delivery: suppressed
AO/tmux session becomes terminal
isTerminated=true
Any PR merged -> merged
Otherwise -> terminated
Source of truth by frontend surface
| Surface |
Source |
Meaning |
| Kanban column |
Backend-derived session.status |
Current workflow/attention placement |
| Session card badge |
Backend-derived session.status |
Exact session/SCM display state |
| Topbar activity pill |
Raw session.activity |
What the agent runtime is doing now |
| Inspector activity row |
Raw session.activity |
Agent runtime activity timeline |
| Browser working indicator |
Raw session.activity |
Whether the agent is actively working |
| Sidebar session dot |
Raw session.activity |
Compact agent runtime activity |
All mappings should remain centralized in frontend/src/renderer/lib/session-presentation.ts. Components must not add independent status/activity switch statements.
Semantic color system
Color must be paired with text labels and motion/state treatment. It must not be the only carrier of meaning.
Kanban zones and terminal cards
| Visual role |
Dark theme |
Light theme |
Treatment |
| Working |
Teal #36C2B4 |
Teal #087F75 |
Live/operational; active indicator may breathe |
| Needs you |
Amber #F2B84B |
Amber #946200 |
Human attention; static |
| In review |
Blue #5B8DEF |
Blue #255FC4 |
External review/waiting |
| Ready to merge |
Light green #75C98D |
Green #237A3D |
Ready but not completed |
| Merged |
Forest surface #173624, accent #4BAE72 |
Surface #D9F2E1, accent #176B37 |
Completed successfully; use readable accent on dark surface |
| Idle |
Gray #8E96A3 |
Gray #69717D |
Quiet nested stack |
| Terminated |
Graphite #6F7680 |
Graphite #626975 |
Neutral terminal outcome |
Terminated should not use red. Termination can be intentional and is not inherently a failure. Red is reserved for actionable failure states such as Exited and CI failed.
Agent activity pill and sidebar dot
| Activity |
Dark theme |
Light theme |
Motion |
active |
Teal #36C2B4 |
Teal #087F75 |
Breathe/pulse |
idle |
Gray #8E96A3 |
Gray #69717D |
Static |
waiting_input |
Amber #F2B84B |
Amber #946200 |
Static |
blocked |
Amber #F2B84B |
Amber #946200 |
Static |
exited |
Coral red #EE6A6A |
Red #B83232 |
Static |
unknown |
Violet #A78BFA |
Violet #6D46C2 |
Static |
Card-level exceptions inside an attention zone
The column color communicates the high-level attention bucket. The card badge may communicate the exact cause:
ci_failed and exited: red/coral
changes_requested and needs_input: amber
no_signal and unknown: violet
pr_open, draft, and review_pending: blue
approved and mergeable: light green
merged: dark forest-green surface with readable green accent
terminated: graphite
Introduce role-specific status tokens rather than continuing to overload generic warning/success colors:
--color-status-working
--color-status-needs-you
--color-status-in-review
--color-status-ready
--color-status-merged
--color-status-merged-surface
--color-status-idle
--color-status-exited
--color-status-terminated
--color-status-unknown
Both themes must meet contrast requirements for text, dots, borders, and subtle card surfaces.
Live propagation
No new transport is required. The existing path is sufficient:
Agent hook reports activity
-> lifecycle manager persists activity_state
-> SQLite trigger writes session_updated to change_log
-> daemon SSE stream publishes the CDC event
-> frontend debounces invalidation by 150 ms
-> workspace query refetches sessions
-> backend derives the new status
-> board, topbar, sidebar, and inspector rerender
Normal connected behavior should move a card shortly after the activity hook arrives. The existing 15-second workspace polling interval remains the fallback if SSE is unavailable.
Implementation plan
Backend lifecycle and domain
API contract
Frontend presentation
Safety requirements
Required test matrix
Backend status derivation
Lifecycle and safety
Frontend
Event propagation
Acceptance criteria
- A session in In review or Needs you moves to Working as soon as its current agent activity becomes active.
- When that agent becomes idle again, SCM facts determine whether it returns to Needs you, In review, or Ready to merge.
- An idle session without an open PR stays in the nested Idle stack.
- An agent exit does not terminate a still-live tmux/AO session.
- An exited agent is visible in Needs you and its terminal remains inspectable.
- A terminal session is shown as Merged when any attributed PR merged; otherwise it is shown as Terminated.
- Board status remains backend-derived and no derived display status is persisted.
- Topbar and sidebar activity indicators consume the same raw activity truth.
- Working and Needs you are visually distinct at a glance.
- Ready to merge and Merged use related but clearly different green treatments.
- Terminated is neutral graphite, not failure red.
- Exited is actionable coral red.
- Unknown is visible violet and emits diagnostics.
- Existing runtime/worktree safety constraints remain intact.
Related issues
Non-goals
Summary
AO currently has the right raw ingredients for session state, but it combines and presents them with the wrong precedence:
This causes a live agent to remain visually stuck in
In review,Needs you, orReady to mergeafter it resumes work. It also collapses an agent process exiting into the AO/tmux session being terminated, even though the tmux session can remain alive and inspectable.The same underlying ambiguity reaches the UI:
session.status.session.activity.This issue defines one canonical state-priority model, preserves the distinction between agent exit and tmux termination, aligns all affected frontend surfaces, and establishes a semantic color system for both dark and light themes.
User-facing problem
Active work can remain in the wrong Kanban column
Today, once a session owns an open PR, PR/SCM status is evaluated before
activity.active. A session that resumes work can therefore remain:The board no longer answers the most immediate question: "Which agents are working right now?"
Expected behavior:
Agent exit and tmux termination are conflated
These are different facts:
activity = exitedmeans the agent process/runtime inside the tmux session exited.isTerminated = truemeans AO considers the managed session terminal.A terminated tmux/AO session necessarily has no running agent, so termination may normalize activity to
exited.The reverse is not true. An agent can exit while tmux remains alive and leaves an interactive shell behind. That state must remain inspectable and visible as
Exitedin Needs you.Status visuals do not communicate a stable semantic language
The current dark-theme tokens use:
These hues are too similar in compact dots, badges, and column accents. Merged and Terminated also collapse into passive Done styling, while Exited and Unknown are both muted gray in the activity presentation.
Current implementation and root cause
Backend display priority
backend/internal/service/session/status.go::deriveStatus()currently evaluates:Because
activecomes after open PR and merged-PR checks, it cannot produceworkingwhile an open PR exists.Lifecycle collapse
backend/internal/lifecycle/manager.go::ApplyActivitySignal()currently does this forActivityExited:This destroys the valid combination:
The storage model already persists
activity_stateandis_terminatedindependently, so no schema change is needed to represent this correctly.Existing safety behavior
backend/internal/sessionguard/guard.goalready refuses both user/API delivery and AO-generated nudges whenactivity = exited, even ifisTerminated = false.That check must remain. After the agent exits, the pane may contain an interactive shell; automatically pasting an agent prompt into it could execute the prompt as shell commands.
Important lifecycle caveat
In the current code,
MarkTerminatedupdates durable state without necessarily tearing down the external runtime/worktree. Merge and tracker completion can call this flag-only path.Physical runtime/worktree cleanup is already tracked separately in:
This issue must not duplicate that cleanup work. It should establish the state contract that an agent-exit hook alone never marks the AO session terminated. Any claim that
isTerminatedliterally proves tmux destruction depends on the teardown issues above being resolved.Canonical concepts
Agent activity
Raw, persisted activity reported by the agent hook pipeline:
Frontend-only
unknownremains a defensive fallback for missing, invalid, or version-skewed data. It is not a backend business state.Session status
A display status derived by the backend at read time from:
activity_stateis_terminatedDerived status must not be stored.
Attention zone
The frontend grouping derived from backend
session.status:Idle remains a nested stack at the bottom of Working, not a separate top-level column.
Required priority and board placement
mergedterminatedworkingexitedneeds_inputneeds_inputidleAdditional defensive behavior:
no_signalremains in Needs you.unknownremains visible in Needs you and should produce telemetry/logging.mergedas a defensive fallback, butactivity.activestill wins while the live agent is working.Proposed backend derivation
Transition examples
Agent resumes from In review
Agent resumes from Needs you
The same applies when the session was in Needs you because CI failed or changes were requested: active work moves it to Working; once the agent returns to idle, SCM facts determine its column again.
Agent becomes idle after opening a PR
Agent becomes idle without a PR
Agent exits but tmux remains
AO/tmux session becomes terminal
Source of truth by frontend surface
session.statussession.statussession.activitysession.activitysession.activitysession.activityAll mappings should remain centralized in
frontend/src/renderer/lib/session-presentation.ts. Components must not add independent status/activity switch statements.Semantic color system
Color must be paired with text labels and motion/state treatment. It must not be the only carrier of meaning.
Kanban zones and terminal cards
#36C2B4#087F75#F2B84B#946200#5B8DEF#255FC4#75C98D#237A3D#173624, accent#4BAE72#D9F2E1, accent#176B37#8E96A3#69717D#6F7680#626975Terminated should not use red. Termination can be intentional and is not inherently a failure. Red is reserved for actionable failure states such as Exited and CI failed.
Agent activity pill and sidebar dot
active#36C2B4#087F75idle#8E96A3#69717Dwaiting_input#F2B84B#946200blocked#F2B84B#946200exited#EE6A6A#B83232unknown#A78BFA#6D46C2Card-level exceptions inside an attention zone
The column color communicates the high-level attention bucket. The card badge may communicate the exact cause:
ci_failedandexited: red/coralchanges_requestedandneeds_input: amberno_signalandunknown: violetpr_open,draft, andreview_pending: blueapprovedandmergeable: light greenmerged: dark forest-green surface with readable green accentterminated: graphiteIntroduce role-specific status tokens rather than continuing to overload generic warning/success colors:
Both themes must meet contrast requirements for text, dots, borders, and subtle card surfaces.
Live propagation
No new transport is required. The existing path is sufficient:
Normal connected behavior should move a card shortly after the activity hook arrives. The existing 15-second workspace polling interval remains the fallback if SSE is unavailable.
Implementation plan
Backend lifecycle and domain
IsTerminated = truesolely because anActivityExitedhook arrived.activity=exited, isTerminated=falsewhile tmux/AO runtime remains available.exitedwhen the AO session is actually marked terminal.domain.StatusExited.exitedto the session API status enum.deriveStatus()soactivewins over all non-terminal SCM/input display states.waiting_inputandblockedmapped toneeds_inputwhenever the current activity remains paused on the user.no_signalbehavior unchanged.AGENT_EXITEDsend error over inaccurately reporting an exited-but-live-tmux session asSESSION_TERMINATED.API contract
npm run api.Frontend presentation
exitedtoSessionStatusparsing and defensive conversion.status=exitedto the Needs you attention zone.Exitedcard badge.session.status.session.activity.unknown.Safety requirements
session.status.Required test matrix
Backend status derivation
workingworkingworkingworkingworkingworkingworkingneeds_inputneeds_inputworkingidleexitedmergedterminatedLifecycle and safety
activity=exitedwithout settingisTerminatedisTerminated=trueand activityexitedFrontend
exitedparses as a real session status, notunknownEvent propagation
session_updatedAcceptance criteria
Related issues
waiting_input. The priority in this issue should supersede/re-evaluate that expectation: current active work wins, while a genuinely current waiting/blocked state remains Needs you.MarkTerminatedskips runtime/worktree teardown.Non-goals