# Agent Orchestrator backend architecture

The backend is a long-running Go daemon that supervises coding-agent sessions.
The current model is intentionally small: session rows persist only durable facts,
and display status is derived at read time.

## Mental model

```
OBSERVE external facts → UPDATE durable facts → DERIVE display status / ACT
```

The durable session facts are:

- `activity_state` — what the agent last reported or what the runtime observer
  can safely conclude (`active`, `idle`, `waiting_input`, `exited`).
- `is_terminated` — whether the session should be treated as over.
- PR facts in the `pr`, `pr_checks`, and `pr_comment` tables.

The UI status is not stored. `service.Session` computes it from the session
record plus PR facts while assembling controller-facing read models.

## Package layout

```
backend/internal/domain           shared vocabulary and API status value types
backend/internal/ports            inbound/outbound interfaces
backend/internal/service/{project,session,pr,review}
                                  controller-facing services and read-model assembly
backend/internal/session_manager  internal session command manager
backend/internal/lifecycle        runtime/activity/spawn/termination session fact reducer
backend/internal/observe/scm      SCM (GitHub) observer loop feeding PR facts
backend/internal/observe/reaper   runtime liveness observation loop
backend/internal/storage          SQLite persistence and DB-triggered CDC
backend/internal/cdc              change-log poller and broadcaster
backend/internal/httpd            daemon HTTP surface (REST + SSE + terminal mux)
backend/internal/terminal         WebSocket terminal multiplexer
backend/internal/adapters         agent/Zellij/git-worktree/GitHub SCM + tracker adapters
backend/internal/daemon           production wiring and shutdown
backend/internal/config           daemon env/default config
```

## Status derivation

`service.Session` selects the display PR from all PR snapshots for a session, then
applies this rough precedence:

1. `is_terminated` → `terminated`, except merged PRs display `merged`.
2. `activity_state=waiting_input` → `needs_input`.
3. Open PR facts drive PR pipeline statuses: `ci_failed`, `draft`,
   `changes_requested`, `mergeable`, `approved`, `review_pending`, `pr_open`.
4. `activity_state=active` → `working`.
5. A signal-capable harness that has never sent a hook callback past the
   ~90s spawn grace → `no_signal` (a broken hook pipeline is visible rather
   than reported as a confident `idle`). Hook-less harnesses stay `idle`.
6. Everything else → `idle`.

## Lifecycle manager

`lifecycle.Manager` is the write path for session lifecycle facts and lifecycle-owned agent nudges:

- runtime observations can mark a session terminated only when runtime and
  process are both clearly dead and recent activity does not contradict that;
  failed/unknown probes do not persist a special state.
- activity signals update `activity_state`; `exited` also marks the session
  terminated.
- PR observations do not write PR rows here, but after the PR service persists
  them lifecycle sends actionable agent nudges for CI failures, review feedback,
  and merge conflicts.

## PR manager

`pr.Manager` records SCM observations into the PR/check/comment tables, then
forwards the observation to lifecycle for agent nudges. A merged PR marks the
owning session terminated through the lifecycle manager; other PR facts are
consumed at read time for display status.

## Session manager

`session_manager.Manager` performs internal session mutations:

- `Spawn` creates a row, creates workspace/runtime resources, and reports the
  handles to the lifecycle manager.
- `Kill` marks the row terminated, then tears down runtime/workspace resources.
- `Restore` relaunches a terminated session and clears `is_terminated` via the
  spawn-completed path.

`service.Session` is the controller-facing boundary. It delegates commands to
`session_manager.Manager` and attaches derived display status on read paths.

## Persistence and CDC

SQLite is the durable store. User-visible table changes are captured by database
triggers into `change_log`; the Go store does not manually emit CDC events. A
poller tails `change_log` and publishes live events to in-process subscribers.

## Load-bearing rules

- Do not store display status.
- Keep session status facts small: `activity_state`, `is_terminated`, and PR
  facts are the durable inputs.
- Do not treat failed probes as death.
- Do not force-delete registered dirty worktrees.
