Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
81 changes: 81 additions & 0 deletions .spec/SessionBackends/00-overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Session Backend Abstraction — Design Overview

## Spec Documents

| Doc | Contents |
|-----|----------|
| [01-tmux-audit.md](01-tmux-audit.md) | Every tmux touchpoint in the codebase, categorized |
| [02-session-backend-interface.md](02-session-backend-interface.md) | `SessionBackend` interface, data model changes, migration plan |
| [03-tmux-backend.md](03-tmux-backend.md) | `TmuxSessionBackend` implementation, method mapping, file layout |
| [04-ambient-backend.md](04-ambient-backend.md) | `AmbientSessionBackend` implementation, API mapping, behavioral differences |

## Summary

### What exists today

- **tmux hardcoded everywhere**: 10+ functions in `tmux.go`, called directly from lifecycle
handlers, liveness loop, broadcast, introspect, approve, and reply.
- **Nascent `AgentBackend` interface** in `agent_backend.go` with `Spawn/Stop/List/Name`. Only
used by `handleCreateAgents`. All other code bypasses it.
- **`AgentUpdate.TmuxSession`** is the field that links an agent to its session. Used across
types, DB models, handlers, frontend, scripts, and docs.

### What this design introduces

- **`SessionBackend` interface** with 13 methods covering the full surface: identity
(`Name`, `Available`), lifecycle (`CreateSession`, `KillSession`, `SessionExists`,
`ListSessions`), status (`GetStatus`), observability (`IsIdle`, `CaptureOutput`,
`CheckApproval`), interaction (`SendInput`, `Approve`, `Interrupt`), and discovery
(`DiscoverSessions`).
- **`TmuxSessionBackend`** — pure wrapper around existing tmux functions. Zero behavior change.
- **`AmbientSessionBackend`** — backed by the ACP public API (`POST /sessions`,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this requires ambient-code/platform#855 to be merged and assumes no major changes to the openapi spec.

`POST /message`, `GET /output`, `POST /stop`, `POST /interrupt`, etc.).
- **`AgentUpdate.SessionID`** + **`AgentUpdate.BackendType`** — replaces `TmuxSession` with
backend-agnostic fields. Backward-compatible JSON unmarshaling for old `tmux_session` payloads.
- **`Server.backends`** registry — map of backend name to implementation. Agents carry their
backend type; the server resolves the right implementation per-agent.
- **`SessionStatus`** enum — unified status model (`unknown`, `pending`, `running`, `idle`,
`completed`, `failed`, `missing`) that both backends map into.

### Interface at a glance

```go
type SessionBackend interface {
Name() string
Available() bool

CreateSession(ctx context.Context, opts SessionCreateOpts) (string, error)
KillSession(ctx context.Context, sessionID string) error
SessionExists(sessionID string) bool
ListSessions() ([]string, error)

GetStatus(ctx context.Context, sessionID string) (SessionStatus, error)

IsIdle(sessionID string) bool
CaptureOutput(sessionID string, lines int) ([]string, error)
CheckApproval(sessionID string) ApprovalInfo

SendInput(sessionID string, text string) error
Approve(sessionID string) error
Interrupt(ctx context.Context, sessionID string) error

DiscoverSessions() (map[string]string, error)
}
```

### Scope of changes

| Area | Files affected | Nature of change |
|------|---------------|-----------------|
| Interface definition | New: `session_backend.go` | New file |
| Tmux backend | New: `session_backend_tmux.go` | Wraps existing functions |
| Old backend | Delete: `agent_backend.go` | Superseded |
| Tmux primitives | `tmux.go` | Unchanged (kept as unexported helpers) |
| Data model | `types.go`, `db/models.go`, `db/convert.go`, `db_adapter.go` | Rename `TmuxSession` -> `SessionID`, add `BackendType` |
| Server | `server.go` | Add `backends` map, `backendFor()` helper |
| Lifecycle | `lifecycle.go` | Route through backend |
| Liveness | `liveness.go` | Route through backend |
| Handlers | `handlers_agent.go` | Route through backend, rename API endpoint |
| Broadcast | `tmux.go` (orchestration funcs) | Route through backend |
| Frontend | `types/index.ts`, `AgentDetail.vue`, `client.ts` | Rename `tmux_session` -> `session_id` |
| Tests | `server_test.go`, `hierarchy_test.go`, `lifecycle_test.go` | Update field names, add mock backend tests |
110 changes: 110 additions & 0 deletions .spec/SessionBackends/01-tmux-audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Tmux Usage Audit

Every place in the codebase that directly touches tmux, categorized by purpose.

## 1. Low-Level Tmux Primitives (`tmux.go`)

| Function | What it does | Called by |
|----------|-------------|-----------|
| `tmuxAvailable()` | Checks if `tmux` binary is in PATH | `TmuxAutoDiscover`, `BroadcastCheckIn`, `SingleAgentCheckIn`, `checkAllSessionLiveness` |
| `tmuxListSessions()` | Runs `tmux list-sessions -F #S` | `tmuxSessionExists`, `TmuxAutoDiscover` |
Copy link
Author

@tiwillia tiwillia Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assumes all tmux sessions are used by agent-boss. Do we need some sort of filtering / tagging mechanism?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely

| `tmuxSessionExists(session)` | Checks if a named session is in the list | `handleAgentSpawn`, `handleAgentStop`, `handleAgentRestart`, `handleAgentIntrospect`, `BroadcastCheckIn`, `handleApproveAgent`, `handleReplyAgent`, `handleSpaceTmuxStatus`, `TmuxBackend.Spawn`, `TmuxBackend.Stop`, `checkAllSessionLiveness` |
| `tmuxCapturePaneLines(session, n)` | Runs `tmux capture-pane -t session -p`, returns last N non-empty lines | `tmuxIsIdle`, `tmuxCheckApproval`, `handleAgentIntrospect`, `handleSpaceTmuxStatus` |
| `tmuxCapturePaneLastLine(session)` | Wrapper: captures last 1 line | `handleSpaceTmuxStatus` |
| `tmuxIsIdle(session)` | Checks last 10 lines for idle indicators (shell prompts, Claude Code `>` prompt, etc.) | `tmuxCheckApproval`, `BroadcastCheckIn`, `checkAllSessionLiveness` |
| `tmuxCheckApproval(session)` | Scans pane for "Do you want...?" + numbered choices pattern | `checkAllSessionLiveness`, `handleAgentIntrospect`, `handleApproveAgent`, `handleSpaceTmuxStatus` |
| `tmuxApprove(session)` | Sends `Enter` key to session | `handleApproveAgent` |
| `tmuxSendKeys(session, text)` | Sends text + `C-m` (Enter) to session | `runAgentCheckIn`, `handleAgentSpawn`, `handleAgentRestart`, `handleReplyAgent`, `handleCreateAgents` (ignite), `TmuxBackend.Spawn` |
| `parseTmuxAgentName(session)` | Extracts agent name from `agentdeck_{name}_{id}` pattern | `TmuxAutoDiscover` |

## 2. Idle Detection Helpers (`tmux.go`)

| Function | What it does |
|----------|-------------|
| `lineIsIdleIndicator(line)` | Returns true if a line matches known idle patterns: `>`, shell `$`/`%`/`#`, Claude Code hints, status bars |
| `isShellPrompt(line)` | Detects `$`, `%`, `>`, `#`, `>` as trailing prompt characters |
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is hilariously brittle. Fine if there is no formal way to determine, but would prefer something that doesn't assume PS1 follows convention.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strongly agree. Perhaps using hooks would be a cleaner approach. https://code.claude.com/docs/en/hooks

| `waitForIdle(session, timeout)` | Polls `tmuxIsIdle` every 3s until idle or timeout |
| `waitForBoardPost(space, agent, since, timeout)` | Polls `agentUpdatedAt` every 3s (not tmux-specific, but used exclusively by broadcast which is tmux-only) |

## 3. Broadcast / Check-In (`tmux.go`)

| Function | What it does |
|----------|-------------|
| `runAgentCheckIn(space, agent, tmuxSession, checkModel, workModel, result)` | Switches model, sends `/boss.check`, waits for board post, restores model. All via `tmuxSendKeys` + `waitForIdle`. |
| `BroadcastCheckIn(space, checkModel, workModel)` | Iterates all agents with `TmuxSession`, calls `runAgentCheckIn` concurrently. |
| `SingleAgentCheckIn(space, agent, checkModel, workModel)` | Single-agent version of broadcast. |
| `BroadcastResult` + helpers | Result accumulator for sent/skipped/errors. |

## 4. Lifecycle Handlers (`lifecycle.go`)

| Handler | Tmux operations performed |
|---------|--------------------------|
| `handleAgentSpawn` | `tmuxSessionExists`, `exec tmux new-session -d`, `tmuxSendKeys` (command), `tmuxSendKeys` (ignite) |
| `handleAgentStop` | Gets `agent.TmuxSession`, `tmuxSessionExists`, `exec tmux kill-session` |
| `handleAgentRestart` | Gets `agent.TmuxSession`, `tmuxSessionExists`, `exec tmux kill-session`, `exec tmux new-session`, `tmuxSendKeys` (command + ignite) |
| `handleAgentIntrospect` | Gets `agent.TmuxSession`, `tmuxSessionExists`, `tmuxIsIdle`, `tmuxCapturePaneLines`, `tmuxCheckApproval` |
| `isNonTmuxAgent(agent)` | Checks `agent.Registration.AgentType != "tmux"` to gate lifecycle endpoints |
| `nonTmuxLifecycleError(w, type)` | Returns 422 for non-tmux agents hitting tmux-only endpoints |
| `inferAgentStatus(exists, idle, needsApproval)` | Pure function mapping booleans to string status (not tmux-specific logic) |

## 5. Liveness Loop (`liveness.go`)

| Function | Tmux operations performed |
|----------|--------------------------|
| `checkAllSessionLiveness` | `tmuxAvailable`, iterates all agents with `TmuxSession`, calls `tmuxSessionExists`, `tmuxIsIdle`, `tmuxCheckApproval`. Updates `InferredStatus`, records interrupts, triggers nudges. Broadcasts SSE `tmux_liveness` event. |
| `executeNudge` | Calls `SingleAgentCheckIn` (which uses tmux) |

## 6. Agent Handlers (`handlers_agent.go`)

| Handler | Tmux operations performed |
|---------|--------------------------|
| `handleSpaceAgent` (POST) | Preserves `TmuxSession` as sticky field on agent update |
| `handleIgnition` (GET) | Accepts `?tmux_session=` query param, stores on agent record, references it in ignition text |
| `handleSpaceTmuxStatus` (GET) | `TmuxAutoDiscover`, iterates agents, calls `tmuxSessionExists`, `tmuxIsIdle`, `tmuxCapturePaneLastLine`, `tmuxCheckApproval` |
| `handleApproveAgent` (POST) | Gets `agent.TmuxSession`, `tmuxSessionExists`, `tmuxCheckApproval`, `tmuxApprove` |
| `handleReplyAgent` (POST) | Gets `agent.TmuxSession`, `tmuxSessionExists`, `tmuxSendKeys` |
| `handleCreateAgents` (POST) | Uses `AgentBackend` interface for spawn, but then calls `tmuxSendKeys` directly for ignite |

## 7. Backend Interface (`agent_backend.go`) — already exists but incomplete

| Method | `TmuxBackend` impl | `CloudBackend` impl |
|--------|-------------------|---------------------|
| `Name()` | `"tmux"` | `"cloud"` |
| `Spawn(ctx, spec)` | Creates tmux session, sends command | Returns `ErrNotImplemented` |
| `Stop(ctx, space, name)` | Kills tmux session | Returns `ErrNotImplemented` |
| `List(ctx, space)` | Lists all tmux sessions | Returns `ErrNotImplemented` |

**Only used by:** `handleCreateAgents`. All other lifecycle/liveness/broadcast code bypasses this interface entirely.

## 8. Data Model References

| Location | Field | Notes |
|----------|-------|-------|
| `types.go:96` | `AgentUpdate.TmuxSession` | JSON tag `tmux_session` |
| `db/models.go:82` | `Agent.TmuxSession` | SQLite column |
| `db/convert.go:35` | `AgentRow.TmuxSession` | DB-to-coordinator conversion |
| `db/convert.go:61,113` | `FromAgentFields(..., tmuxSession, ...)` | Coordinator-to-DB conversion |
| `db_adapter.go:317,371` | References `TmuxSession` | DB adapter layer |
| `db/migrate_from_json.go:40` | `jsonAgent.TmuxSession` | JSON migration |

## 9. Frontend References

| File | Usage |
|------|-------|
| `frontend/src/types/index.ts:49,118` | `tmux_session?: string` on agent types |
| `frontend/src/api/client.ts:257,275` | Spawn/restart return `tmux_session` |
| `frontend/src/components/AgentDetail.vue:133,562,918,921` | Displays tmux session, gates pane/controls sections |

## 10. Scripts and Documentation

| File | Usage |
|------|-------|
| `scripts/boss.sh` | `get_tmux_session()`, passes `-e TMUX_SESSION` |
| `scripts/agent-ignition.sh` | `create_tmux_session()` |
| `scripts/coordination-client.py` | Passes `tmux_session` to ignition |
| `commands/boss.ignite.md` | References `?tmux_session=` |
| `commands/boss.check.md` | Notes `tmux_session` is sticky |
| `docs/AGENT_PROTOCOL.md` | Documents `tmux_session` field, ignition params |
| `docs/lifecycle-spec.md` | Spawn/stop/restart reference `tmux_session` |
| `docs/api-reference.md` | API docs reference `tmux_session` |
| `docs/hierarchy-design.md` | Compares parent stickiness to `TmuxSession` |
Loading