Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 34 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,11 +214,14 @@ includes the explicit backgrounds used for consistent terminal rendering.
only the local measurement interval and does not use a quota-reset token.
Each independent local root session gets its own metrics and 30-second graph;
explicitly linked spawned agents are included with their root.
Terminal session telemetry boxes keep their proportional width on narrow
screens but cap at 48 columns, leaving extra width for detail and graphs.
- Dismissible session rows that automatically return on fresh activity,
without closing or modifying the underlying Codex session.
- Highlighted per-session attention badges. A shared Codex app-server supplies
exact `INPUT NEEDED` and `APPROVAL NEEDED` states. Without it, a completed
open turn is definite `INPUT NEEDED`; an otherwise active session with no
open turn is `TURN COMPLETE`, not a request for input; an otherwise active
session with no
rollout activity for three minutes is cautiously labelled `CHECK SESSION`.
For a root with linked agents, fresh activity from any member suppresses that
uncertain fallback; definite input or approval signals still propagate from
Expand Down Expand Up @@ -1168,7 +1171,7 @@ The other top-level views are:
session. It automatically establishes a zero baseline across locally active
Codex sessions when Codexometer starts. The **SESSION TOTALS** readout follows newly appended
token telemetry in a compact summary strip: **TOKENS**, visible **SESSIONS**,
**WORKING**, **APPROVAL**, **INPUT**, and **CHECK*** counts. CHECK* means inferred
**ACTIVE**, **WORKING**, **APPROVAL**, **INPUT**, **ERRORS**, and **CHECK*** counts. CHECK* means inferred
inactivity, not a confirmed request. Linked agents are already included in
parent sessions and are not counted again. Tokens retain the existing measurement
baseline (including previously dismissed sessions); the session/state counts
Expand Down Expand Up @@ -1213,6 +1216,19 @@ The other top-level views are:
minutes receives `CHECK SESSION`: a deliberately uncertain prompt that can
also mean a long-running local tool. Subsequent activity from the root or any
linked agent clears the uncertain group-level warning.
Confirmed failed turns instead enter **ERRORS**, with an amber **MODEL BUSY**
badge for server-reported model capacity and **TURN FAILED** for other failures.
The row, full detail and attention pill retain the observed error message;
capacity is not interpreted as quota exhaustion. Both the shared app-server's
failed-turn/error events and local rollout error records are supported. Errors
that Codex is still automatically retrying remain **WORKING**, as do non-turn
failures such as a rejected steering attempt. A new turn on the affected
thread clears its prior failure;
stale events from older identified turns cannot fail the new one. Errors from
linked agents propagate to the parent row, below pending approvals and questions.
No prompt is automatically retried and no model is automatically switched.
**INPUT** is reserved for observed blocking questions/requests, not a completed
turn without a final reply. Normal completion remains **TURN COMPLETE**.
Codexometer never guesses `APPROVAL NEEDED` from inactivity. Closing the CLI
releases its per-thread writer lock and clears every attention badge.
A session already included in the current Sessions recording can remain as an
Expand Down Expand Up @@ -1590,7 +1606,8 @@ live session states and are not persisted across launches.

**Active** includes sessions identified as active by the latest observation,
including working sessions and those waiting for input, approval or their next
turn. It excludes inferred **CHECK SESSION** entries and retained inactive rows.
turn. It excludes confirmed failures, inferred **CHECK SESSION** entries and
retained inactive rows.
An app-server-confirmed loaded, idle/ready session remains Active even after
five minutes without session-file updates; that confirmation is not carried
forward if it disappears or the daemon disconnects. Without the daemon, the
Expand All @@ -1615,7 +1632,8 @@ Enter submits, and `Esc` leaves the editor first. Approval controls remain
exclusive to the current target.

An **attention strip** beneath the summary provides direct links to visible
sessions, ordered **Approval → Input → Turn Complete**. Approval and input pills
sessions, ordered **Approval → Input → Errors → quota-threshold reviews → Turn Complete → triggers**.
Approval, input and error pills
use the theme's warning colour; completed turns use a quieter, non-bold theme-primary
colour to mean “your result is ready”, not “a reply is required”. Completion pills
disappear when work resumes, the completion state clears or the session is dismissed.
Expand Down Expand Up @@ -1736,6 +1754,8 @@ The action line shows only the current next step, Enter Review or C Confirm,
without a second confirmation hint. Settings blocked during an active turn show
**WAITING FOR IDLE**, with **Ctrl+R REFRESH** in the terminal composer
(**F REFRESH** in the read-only picker) to read their availability again;
speed changes are available during a turn and are sent immediately after confirmation,
without a local queue. They configure subsequent turns, not a request already in flight.
**HELP** identifies genuine browse-only entries. Model, reasoning and speed changes are provisional until C;
reviewing or changing the highlight sends no settings update. Buttons
are clickable. Writable web mode exposes the same catalogue through
Expand Down Expand Up @@ -1816,9 +1836,12 @@ for the upstream catalogues.

Renames require a loaded session and explicit confirmation within 30 seconds; a concurrent rename or reconnect invalidates the confirmation. The updated name appears on the next telemetry refresh.

Codex session-setting changes require a loaded, idle session and explicit confirmation within
30 seconds. The command and options are revalidated before sending; stale
connections, pending approvals and changed options fail closed. A successful
Codex session-setting changes require a loaded session and explicit confirmation within
30 seconds. Speed-only changes can be sent while working or waiting for an approval;
other settings still require idle and no pending approval. The command and options
are revalidated before sending; stale connections and changed options fail closed.
A normal turn start/completion does not invalidate a speed confirmation, but changed
model, reasoning or speed settings do. A successful
request is an acknowledgement, not a claim that a subsequent turn has already
used the setting. Successful confirmations close the menu and return focus to
the composer with a success notice; reopening fetches fresh options. Failed
Expand Down Expand Up @@ -2916,7 +2939,10 @@ available from the existing reader.
- Attention links jump to the relevant session. Approval indicators remain
outside scrollable reply text; available commands are separated from their
justification. Missing commands are explicitly labelled, never reconstructed.
- **INPUT NEEDED / APPROVAL NEEDED** represent observed signals; **CHECK SESSION**
- **INPUT NEEDED / APPROVAL NEEDED** represent observed signals; **MODEL BUSY /
TURN FAILED** represent confirmed turn errors and appear in **ERRORS**, with
the observed explanation in detail. Retrying errors do not raise failure pills.
**CHECK SESSION**
is labelled inferred inactivity and is not proof that a reply is required.
**TURN COMPLETE** is informational. Disconnection or failed session refresh
suppresses live attention indicators and marks the display stale.
Expand Down
2 changes: 1 addition & 1 deletion docs/slash-command-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Local Codexometer support below was updated on **2026-10-11**; the upstream regi
| `/exit`, `/quit` | Local equivalents | Done | — | Terminal aliases reuse local Quit. Browser aliases leave full session detail for Sessions; the web server and Codex sessions remain running. |
| `/statusline` | Local equivalent | Done | — | Codexometer's own footer picker; also available in read-only web mode. Does not edit the native CLI's status line. |
| `/help` | Local catalogue | Done | — | Codexometer catalogue entry; absent from the upstream registry. |
| Advertised speed aliases, e.g. `/fast` | Session adapter | Done | — | Show all live model-advertised speeds plus the default in one picker; Up/Down selects, Enter Review stages the choice, and C Confirm commits a verified session override. Global defaults remain out of scope. |
| Advertised speed aliases, e.g. `/fast` | Session adapter | Done | — | Show all live model-advertised speeds plus the default in one picker; Up/Down selects, Enter Review stages the choice, and C Confirm sends a verified session override immediately, even during a turn or pending approval. Applies to subsequent turns, not an inference request already in flight; no local queue or global-default changes. The composer retains the public alias rather than the internal tier path. |

The local `/usage`, `/copy`, `/theme`, `/exit`, `/quit` and `/statusline` entries are merged into the interface catalogue and do not become server RPCs. They work without a shared Codex daemon; browser full detail offers the local catalogue in read-only mode. Slash names are reserved against model-advertised speed aliases.

Expand Down
1 change: 1 addition & 0 deletions internal/codex/daemon_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const (
sessionRuntimeInput
sessionRuntimeApproval
sessionRuntimeComplete
sessionRuntimeFailed
)

type sessionStatusProvider interface {
Expand Down
3 changes: 3 additions & 0 deletions internal/codex/daemon_status_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@ func (p *daemonStatusProvider) statusSnapshotLocked(threadIDs []string) sessionD
if state := p.contexts[threadID]; status == sessionRuntimeIdle && state != nil && state.completed {
status = sessionRuntimeComplete
}
if state := p.contexts[threadID]; status == sessionRuntimeIdle && state != nil && state.latest.Kind == SessionContextFailure {
status = sessionRuntimeFailed
}
statuses[threadID] = status
if cwd := p.threadDirectories[threadID]; cwd != "" {
directories[threadID] = cwd
Expand Down
69 changes: 52 additions & 17 deletions internal/codex/live_usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ type LiveUsageSession struct {
// Input and approval are definite signals; Check is deliberately cautious and
// means only that an open fallback-observed session has remained quiet.
// Complete is informational: an observed completed turn, not an input request.
// Error is a confirmed failed turn; it is never inferred from inactivity.
type SessionAttention int

const (
Expand All @@ -88,6 +89,7 @@ const (
SessionAttentionApproval
SessionAttentionCheck
SessionAttentionComplete
SessionAttentionError
)

// LiveModelCall is a small, content-free response-usage pulse. APIEqIssue retains
Expand Down Expand Up @@ -213,6 +215,7 @@ const (
sessionAttentionIdle
sessionAttentionInput
sessionAttentionApproval
sessionAttentionFailed
)

type rolloutEvent struct {
Expand Down Expand Up @@ -293,8 +296,11 @@ type rolloutAttentionEvent struct {
Ordinal *uint64 `json:"ordinal"`
Type string `json:"type"`
Payload struct {
Type string `json:"type"`
IsBlocking *bool `json:"isBlocking"`
Type string `json:"type"`
IsBlocking *bool `json:"isBlocking"`
Error json.RawMessage `json:"error"`
WillRetry *bool `json:"will_retry"`
TurnID string `json:"turn_id"`
} `json:"payload"`
}

Expand Down Expand Up @@ -615,6 +621,13 @@ func (r *LiveUsageReader) addFile(path string, info os.FileInfo) error {
nonRoot: metadata.NonRoot,
subagentHistoryStartOrdinal: metadata.SubagentHistoryStartOrdinal,
}
// Both bootstrap scans reject late failures from older turns. Establish
// their turn scope first, including when discovery finds a new rollout.
turnID, err := latestRolloutTurnID(path, cursor)
if err != nil {
return err
}
cursor.currentTurnID = turnID
if time.Since(info.ModTime()) <= activeRolloutHorizon {
attention, err := latestSessionAttention(path, cursor)
if err != nil {
Expand All @@ -640,11 +653,6 @@ func (r *LiveUsageReader) addFile(path string, info os.FileInfo) error {
return modelErr
}
cursor.currentModel = model
turnID, turnErr := latestRolloutTurnID(path, cursor)
if turnErr != nil {
return turnErr
}
cursor.currentTurnID = turnID
total, err := latestTokenTotal(path)
if err != nil {
return err
Expand Down Expand Up @@ -748,7 +756,7 @@ func (r *LiveUsageReader) consume(path string, cursor *rolloutCursor) error {
cursor.currentModel = model
cursor.currentServiceTier = cursor.nextServiceTier
}
if attention, ordinal, at, ok := sessionAttentionRecord(line); ok &&
if attention, ordinal, at, ok := sessionAttentionRecord(line, cursor.currentTurnID); ok &&
tokenRecordIsOwned(ordinal, cursor.subagentHistoryStartOrdinal, at, cursor.nonRoot, cursor.startedAt) {
cursor.attention = attention
}
Expand Down Expand Up @@ -988,7 +996,7 @@ func latestSessionAttention(path string, cursor *rolloutCursor) (sessionAttentio
firstComplete = 0
}
for index := len(lines) - 1; index >= firstComplete; index-- {
attention, ordinal, at, ok := sessionAttentionRecord(lines[index])
attention, ordinal, at, ok := sessionAttentionRecord(lines[index], cursor.currentTurnID)
Comment thread
merefield marked this conversation as resolved.
Resolved
if ok && tokenRecordIsOwned(ordinal, cursor.subagentHistoryStartOrdinal, at, cursor.nonRoot, cursor.startedAt) {
return attention, nil
}
Expand All @@ -998,7 +1006,7 @@ func latestSessionAttention(path string, cursor *rolloutCursor) (sessionAttentio
return sessionAttentionUnknown, nil
}

func sessionAttentionRecord(line []byte) (sessionAttentionState, *uint64, time.Time, bool) {
func sessionAttentionRecord(line []byte, currentTurnID string) (sessionAttentionState, *uint64, time.Time, bool) {
if !bytes.Contains(line, []byte(`"event_msg"`)) && !bytes.Contains(line, []byte(`"response_item"`)) {
return sessionAttentionUnknown, nil, time.Time{}, false
}
Expand All @@ -1012,6 +1020,9 @@ func sessionAttentionRecord(line []byte) (sessionAttentionState, *uint64, time.T
if event.Type != "event_msg" {
return sessionAttentionUnknown, nil, time.Time{}, false
}
if (event.Payload.Type == "error" || event.Payload.Type == "task_complete" || event.Payload.Type == "turn_complete") && currentTurnID != "" && event.Payload.TurnID != "" && currentTurnID != event.Payload.TurnID {
return sessionAttentionUnknown, nil, time.Time{}, false
}
switch event.Payload.Type {
case "request_user_input":
if event.Payload.IsBlocking == nil || *event.Payload.IsBlocking {
Expand All @@ -1020,7 +1031,20 @@ func sessionAttentionRecord(line []byte) (sessionAttentionState, *uint64, time.T
case "exec_approval_request", "apply_patch_approval_request", "request_permissions", "elicitation_request":
return sessionAttentionApproval, event.Ordinal, event.Timestamp, true
case "task_complete", "turn_complete":
if len(event.Payload.Error) > 0 && string(event.Payload.Error) != "null" {
return sessionAttentionFailed, event.Ordinal, event.Timestamp, true
}
return sessionAttentionIdle, event.Ordinal, event.Timestamp, true
case "error":
if event.Payload.WillRetry != nil && *event.Payload.WillRetry {
break
}
var raw struct {
Payload json.RawMessage `json:"payload"`
}
if json.Unmarshal(line, &raw) == nil && failureAffectsTurn(sessionFailureContext(SessionContext{}, raw.Payload)) {
return sessionAttentionFailed, event.Ordinal, event.Timestamp, true
}
case "task_started", "turn_started", "user_message", "exec_command_begin", "dynamic_tool_call_response":
return sessionAttentionWorking, event.Ordinal, event.Timestamp, true
}
Expand Down Expand Up @@ -1415,9 +1439,6 @@ func (r *LiveUsageReader) sessionSnapshots(now time.Time, liveWriters map[string
exactWorking := exact && exactStatus == sessionRuntimeWorking
localWorking := !exact && writerActive && cursor.attention == sessionAttentionWorking
attention := sessionAttention(cursor.attention, writerActive, writerLocksSupported, quietFor, exactStatus, exact)
if !exact && attention == SessionAttentionInput && cursor.attention == sessionAttentionIdle && cursor.preview.Kind == SessionContextReply {
attention = SessionAttentionComplete
}
if !active && !exactWorking && attention == SessionAttentionNone && cursor.observedTokens == 0 && len(cursor.modelCalls) == 0 && len(cursor.turnTimings) == 0 {
continue
}
Expand All @@ -1437,21 +1458,29 @@ func (r *LiveUsageReader) sessionSnapshots(now time.Time, liveWriters map[string
group.Active = group.Active || active || exactWorking || attention != SessionAttentionNone
group.Attention = mergeSessionAttention(group.Attention, attention)
preview := cursor.preview
if exact && (exactStatus == sessionRuntimeWorking || exactStatus == sessionRuntimeComplete) && preview.Kind == SessionContextFailure {
preview = SessionContext{}
}
if exact && preview.pending() {
preview = SessionContext{}
}
if live, ok := r.daemonContexts[cursor.threadID]; ok {
if live.At.After(preview.At) || live.pending() || live.PendingApprovalsLimited {
preview = live
} else if preview.Kind == SessionContextReply && live.ThreadID == cursor.threadID && live.TurnID != "" && live.TurnID == cursor.currentTurnID {
} else if (preview.Kind == SessionContextReply || preview.Kind == SessionContextFailure) && live.ThreadID == cursor.threadID && live.TurnID != "" && live.TurnID == cursor.currentTurnID {
// The local task_complete record can be newer than the live
// final message. Keep its reply, but retain observed prompt
// final message. Keep its reply/error, but retain observed prompt
// context only for the same positively identified turn.
preview.CurrentTask, preview.LatestGuidance = live.CurrentTask, live.LatestGuidance
preview.LatestGuidanceID = live.LatestGuidanceID
preview.TurnID = live.TurnID
}
}
if preview.Kind == SessionContextFailure && (!writerLocksSupported || writerActive || exact) && (!exact || exactStatus != sessionRuntimeWorking && exactStatus != sessionRuntimeComplete) {
attention = mergeSessionAttention(attention, SessionAttentionError)
group.Attention = mergeSessionAttention(group.Attention, attention)
localWorking = false
}
// A persisted request is not proof that it is still outstanding. Match
// it to current attention before displaying it as actionable context.
if preview.Kind == SessionContextApproval && attention != SessionAttentionApproval ||
Expand Down Expand Up @@ -1534,6 +1563,8 @@ func sessionAttention(state sessionAttentionState, writerActive, writerLocksSupp
return SessionAttentionApproval
case sessionRuntimeInput:
return SessionAttentionInput
case sessionRuntimeFailed:
return SessionAttentionError
}
// A status supplied by the shared daemon is authoritative. In
// particular, an active thread without a waiting flag must not age into
Expand All @@ -1546,10 +1577,12 @@ func sessionAttention(state sessionAttentionState, writerActive, writerLocksSupp
switch state {
case sessionAttentionIdle:
if writerActive {
return SessionAttentionInput
return SessionAttentionComplete
}
case sessionAttentionInput:
return SessionAttentionInput
case sessionAttentionFailed:
return SessionAttentionError
case sessionAttentionApproval:
return SessionAttentionApproval
}
Expand All @@ -1569,8 +1602,10 @@ func mergeSessionAttention(current, next SessionAttention) SessionAttention {
func sessionAttentionPriority(attention SessionAttention) int {
switch attention {
case SessionAttentionApproval:
return 4
return 5
case SessionAttentionInput:
return 4
case SessionAttentionError:
return 3
case SessionAttentionCheck:
return 2
Expand Down
2 changes: 1 addition & 1 deletion internal/codex/live_usage_compressed.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ func baselineRolloutRecord(c *rolloutCursor, line []byte) {
if turn, ordinal, at, ok := rolloutTurnIDRecord(line); ok && tokenRecordIsOwned(ordinal, c.subagentHistoryStartOrdinal, at, c.nonRoot, c.startedAt) {
c.currentTurnID = turn
}
if attention, ordinal, at, ok := sessionAttentionRecord(line); ok && tokenRecordIsOwned(ordinal, c.subagentHistoryStartOrdinal, at, c.nonRoot, c.startedAt) {
if attention, ordinal, at, ok := sessionAttentionRecord(line, c.currentTurnID); ok && tokenRecordIsOwned(ordinal, c.subagentHistoryStartOrdinal, at, c.nonRoot, c.startedAt) {
c.attention = attention
}
if preview, ok := rolloutContextRecord(line, c); ok {
Expand Down
Loading
Loading