Skip to content

bug(core): stuck session with exited runtime never auto-promotes to terminated — lingers in sidebar indefinitely #1933

Description

@harshitsinghbhandari

Repro

Spawn a session, let the agent crash or exit without acknowledging (reportWatcherActiveTrigger: no_acknowledge). The lifecycle manager probes, sees signal_disagreement between the native activity signal (activity=exited) and the initial runtime probe (runtime=alive), runs through 50 detecting attempts, escalates, and parks the session at state: stuck, reason: probe_failure. The runtime probe eventually updates to exited, but the session-level state never advances.

Observed state (real session ao-165)

"lifecycle": {
  "session":  { "state": "stuck",  "reason": "probe_failure",  "terminatedAt": null,
                "lastTransitionAt": "2026-05-19T13:14:54.490Z" },
  "runtime":  { "state": "exited", "reason": "process_missing",
                "lastObservedAt": "2026-05-19T13:14:54.490Z" },
  "pr":       { "state": "none",   "reason": "not_created" }
},
"status": "stuck",
"detectingAttempts": "50",
"detectingEscalatedAt": "2026-05-19T12:27:51.146Z",
"lifecycleEvidence": "signal_disagreement runtime=alive process=dead activity_signal=valid via_native activity=exited",
"reportWatcherActiveTrigger": "no_acknowledge",
"reportWatcherTriggerCount": "41"

The agent process is gone, the runtime is gone, no PR was ever created — but the session is not terminated.

Why this is a bug

The sidebar visibility gate in packages/web/src/components/ProjectSidebar.tsx:454-457 only hides sessions whose getAttentionLevel(session) === \"done\". A stuck lifecycle state routes to respond in packages/web/src/lib/types.ts:504-512:

if (session.lifecycle?.sessionState === \"stuck\" || ...) return \"respond\";

So the session sits on the sidebar with an orange dot forever. Neither the Show killed nor the Show done toggle helps — they only gate done-level sessions. The only ways to clear it today:

  • ao kill <id> (manual)
  • Click Restore (spawns a new runtime, leaves the record)
  • rm the JSON by hand

That's not great UX for a session the lifecycle manager has already concluded is unrecoverable.

Proposed fix (core, not surface)

When resolveProbeDecision (in packages/core/src/lifecycle-manager.ts) determines:

  • runtime.state === \"exited\" or \"missing\", and
  • native activity signal is exited, and
  • no live PR depends on the runtime (pr.state ∈ {none, closed_unmerged}),

…it should promote session.state from stuckterminated with reason: probe_failure (or a new agent_process_exited reason). deriveLegacyStatus already maps error_in_process / probe_failureerrored, which is in the done bucket, so the sidebar would then hide it by default once showDone is off.

Today the lifecycle treats probe_failure as "I can't tell, escalate to a human". But when every signal agrees the agent is gone, there's nothing to escalate — just terminate it.

Out of scope (separate concerns)

File:line index

  • Visibility gate: packages/web/src/components/ProjectSidebar.tsx:454-457
  • \"stuck\"\"respond\" mapping: packages/web/src/lib/types.ts:504-512
  • isDashboardSessionDone: packages/web/src/lib/types.ts:392-410
  • deriveLegacyStatus terminal-reason mapping: packages/core/src/lifecycle-state.ts:444-460
  • Probe decision pipeline: packages/core/src/lifecycle-manager.ts (resolveProbeDecision)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions