{run.project_id || run.cwd || "Project unknown"} · {run.git_branch || "Branch unknown"}{run.is_worktree ? " · Worktree" : ""}
+{run.reason}
+ Started {time(run.created_at)} · Last observed {observation(run)} +diff --git a/docs/agent-runs.md b/docs/agent-runs.md
new file mode 100644
index 0000000..4760ac7
--- /dev/null
+++ b/docs/agent-runs.md
@@ -0,0 +1,15 @@
+# Managed runs
+
+Open **Runs** in the sidebar (`#/runs`) to inspect managed Claude and Codex execution attempts. Search across session ID, directory, project, model and branch; filter by provider, state or exact project ID. Results use pages of 50. A run detail URL loads independently of the current page. Source links open the existing chat with its Decoded / Native controls; missing indexed chats retain their run history and show an unavailable-source explanation.
+
+A run represents **one managed process attempt**, spanning multiple prompts. It is not a task or an individual conversation turn. Historical indexed sessions do not become runs until managed execution is requested. Native rollout adoption changes the session ID while keeping the attempt ID. Reattaching to a verified surviving tmux process also keeps the attempt ID. A replaced or confirmed-absent process closes the old attempt with an unknown outcome (`blocked`, with `ended_at`) and creates a fresh attempt. Terminal records cannot be updated by later observations.
+
+States are `queued`, `starting`, `running`, `awaiting_input`, `blocked`, `completed`, `failed`, and `canceled`. Launch begins at `starting`; an attached transport remains `blocked` pending screen evidence. Successful decoded submission or native Enter records `running` with explicit input provenance. Native Enter does not prove what command was entered. Provider screen observations may indicate `awaiting_input`, `running`, or `blocked`, but are advisory: Claude's classifier defaults to idle, and native prompts are not reliable completion signals. Last-observed timestamps reflect real attach, input or screen observations, not a fabricated heartbeat. Old observations can be stale even if a transport remains attached.
+
+Explicit termination records `canceled` after the termination operation returns. Confirmed launch failure records `failed`. A capture/attach failure with a verified surviving pane leaves an active, recoverable `blocked` attempt. Disconnect or server restart marks an attempt `blocked` with an unobserved/detached explanation. Shutdown detaches tmux attempts; direct PTY shutdown closes an unknown-outcome attempt because its process is terminated. No prompt, idle screen, process exit (including exit 0), or disconnect records successful completion. There is deliberately no “finish” operation: this slice does not infer or automate task outcomes. `completed` and `queued` are contract foundations for later explicit outcome/queue features.
+
+The `agent_runs` SQLite table is independent of the rebuildable session index and has no cascading foreign key. Source indexing fills missing metadata while preserving recorded execution model/context. `task_id` and `parent_run_id` are explicitly nullable foundations; no task management, queue launch, or approval automation is implemented.
+
+`GET /api/runs` returns typed `AgentRun[]`; optional `provider`, `state`, `project`, and `q` filters apply before `limit` (1–500, default 200) and `offset`. `GET /api/runs/{run_id}` returns one record or 404. Both endpoints only inspect durable records and indexed metadata; they never start providers or claim execution locks.
+
+The execution manager writes lifecycle records before publishing `runs_changed` SSE notifications. Screen observations enter a manager-owned buffer directly from provider policy, rather than through a lossy SSE subscriber; the latest advisory observation is persisted every two seconds. The UI refreshes on confirmed-write notifications/reconnect and every 15 seconds to recover missed notifications. This is current-state history, not a complete event audit log. A partial unique index and immediate SQLite transaction prevent duplicate active attempts for a provider/session across independent store instances. Execution lifecycle operations and observation writes are serialized by the shared manager.
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 0ed006f..862b714 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -34,6 +34,7 @@ const settingsModalImport = () => import("./components/Settings/SettingsModal");
// render a reload prompt in-place instead of crashing the whole ErrorBoundary.
const Sidebar = lazyWithRetry(() => import("./components/Sidebar/Sidebar"));
const ChatView = lazyWithRetry(chatViewImport);
+const Runs = lazyWithRetry(() => import("./components/Runs"));
const Dashboard = lazyWithRetry(() => import("./components/Dashboard/Dashboard"));
const SettingsModal = lazyWithRetry(settingsModalImport);
const SearchOverlay = lazyWithRetry(searchOverlayImport);
@@ -332,7 +333,7 @@ export default function App() {
Execution history across Claude and Codex.
{error}
} + {loading ?Loading runs…
: <> + {runId && (detail ?Run not found. View all runs
: null)} + {!error && !runs.length &&{filtered ? "No runs match these filters." : "No managed runs yet. Open a session and attach its managed terminal to create a run. Historical chats are not automatically live runs."}
} +{run.project_id || run.cwd || "Project unknown"} · {run.git_branch || "Branch unknown"}{run.is_worktree ? " · Worktree" : ""}
+{run.reason}
+ Started {time(run.created_at)} · Last observed {observation(run)} +An idle prompt or process exit does not establish task success. Live state is best-effort. Check the last observation time; restart or transport loss may leave this attempt detached or unobserved.
} +