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
102 changes: 102 additions & 0 deletions .spec/SessionBackends/00-overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# 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 |
| [05-agentcore-feasibility.md](05-agentcore-feasibility.md) | AWS Bedrock AgentCore feasibility analysis |

## Summary

### What exists today

- **tmux hardcoded everywhere**: 10+ functions in `tmux.go`, called directly from lifecycle
handlers, liveness loop, broadcast, introspect, approve, and reply.
- **`AgentBackend` interface** in `agent_backend.go` (PR #47) 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.
- **`tmuxDefaultSession`** (PR #49, open) proposes space-scoped names `{space}-{agent}` — not adopted here.

### 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`).
- **Role interfaces** (`SessionLifecycle`, `SessionObserver`, `SessionActor`) for
narrow consumer dependencies and easier testing.
- **`TmuxSessionBackend`** — wraps existing tmux functions. Preserves current
`agentdeck_*` naming convention. 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`, `DELETE /sessions/{id}`, `POST /interrupt`, etc.).
Depends on platform PR #855.
- **Subsumes `AgentBackend`** — the existing interface from PR #47 is folded into
`SessionBackend`. `agent_backend.go` is deleted.
- **`AgentUpdate.SessionID`** + **`AgentUpdate.BackendType`** — replaces `TmuxSession`
with backend-agnostic fields. No backward-compat shim (clean break).
- **`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 all backends map into.
- **`BackendOpts interface{}`** — backend-specific creation options. Each backend defines
its own options struct (`TmuxCreateOpts`, `AmbientCreateOpts`), keeping backend-specific
code contained within each backend.

### 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 (folded into SessionBackend) |
| 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 |

### Known gaps (deferred)

| Gap | Notes |
|-----|-------|
| Context/tool injection for Ambient | Ambient sessions don't inherit local boss commands. Needs workflow or MCP server approach. Deferred to Phase 2. |
| Cross-space session name collisions | Current `agentdeck_*` naming doesn't include space. Same agent name in two spaces can collide. PR #49 proposes a fix but is out of scope here. |
| Session ownership/filtering | `tmuxListSessions` returns all sessions, not just agent-boss. Mitigated by naming convention but not solved. |
| Idle detection brittleness | `isShellPrompt` relies on PS1 heuristics. Claude Code hooks would be cleaner. |
| Model switching compaction risk | Switching from opus to haiku with large context triggers compaction. Needs separate evaluation. |
129 changes: 129 additions & 0 deletions .spec/SessionBackends/01-tmux-audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# 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`. **Note:** returns ALL tmux sessions on the machine, not just agent-boss sessions. Needs filtering/tagging mechanism (see §Session Ownership below). | `tmuxSessionExists`, `TmuxAutoDiscover` |
| `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. **Brittle:** assumes PS1 follows convention. A cleaner approach would be using [Claude Code hooks](https://code.claude.com/docs/en/hooks) to emit structured idle/busy signals instead of parsing terminal output. |
| `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` |

## Session Ownership

`tmuxListSessions()` returns ALL tmux sessions on the machine — not just those
created by agent-boss. This means discovery and liveness can incorrectly interact
with unrelated sessions.

Currently, agent-boss sessions are identified by naming convention only:
- Legacy: `agentdeck_{name}_{timestamp}` (parsed by `parseTmuxAgentName`)
- PR #49: `{space}-{agent}` (parsed by space prefix matching)

Neither convention provides a strong ownership guarantee. Options for improvement:
- **tmux environment variable**: set `@agent_boss=true` on sessions at creation,
Copy link
Owner

Choose a reason for hiding this comment

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

This seems like the best proposal

filter by it during listing
- **Dedicated tmux server**: use `tmux -L agent-boss` to isolate sessions entirely
- **Prefix convention**: require a fixed prefix (e.g., `ab-{space}-{agent}`) that
is unlikely to collide with user sessions

This is out of scope for the current refactoring but should be addressed.
Loading