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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
- `agent-tty batch <session-id>`: run an ordered sequence of input-and-`wait` steps against one session in a single invocation, supplied as a positional JSON array or `--file`. Each step is one verb (`type`, `paste`, `sendKeys`, `run`, or `wait`); every `wait` is anchored to a Wait Baseline (the Event Log sequence after the preceding input step) so it cannot match a stale screen the way a hand-written `run`/`wait`/`send-keys` loop can (ADR 0007). Fail-fast by default with a non-zero exit and a per-step `--json` envelope; `--keep-going` attempts every step. SIGINT/SIGTERM flushes a partial envelope (in-flight step `interrupted`, later steps `not-run`). Adds a new `WAIT_TIMEOUT` error and exit code `11` for timed-out wait steps inside a batch ([#126](https://github.com/coder/agent-tty/pull/126), closes [#123](https://github.com/coder/agent-tty/issues/123)).
- Optional `screenHash` on `snapshot` and render-`wait` results (also on matched `batch` wait steps): a lowercase 64-char SHA-256 of the canonical visible-screen text (`visibleLines[].text` joined by `\n`, no scrollback, cursor, or styles). Gives automation a stable token to tell whether the rendered screen actually changed between two observations without diffing full text, and unlike the Event Log sequence it does not advance on cursor moves or no-op repaints. Present on every result that observed a snapshot (live matches, captures, and the offline `matched:false` fallback); absent only when no screen was observed (live timeout, consecutive-failure giveup, replay-error throw). Standalone `wait` adds an `--after-seq` flag, and `type` / `paste` results now return their Event Log `seq` so callers can anchor a following wait themselves ([#127](https://github.com/coder/agent-tty/pull/127), closes [#125](https://github.com/coder/agent-tty/issues/125)).
- `agent-tty d` is now a short alias for `agent-tty dashboard`. It is an explicit alias (not prefix matching), so it resolves unambiguously to the dashboard and never collides with the other `d`-prefixed commands (`destroy`, `doctor`) ([#129](https://github.com/coder/agent-tty/pull/129)).
- **Home Registry + dashboard Home picker**: agent-tty now remembers every **Home** (state root) that has hosted a Session in a per-machine, advisory index at `${XDG_STATE_HOME:-~/.local/state}/agent-tty/homes.json`, auto-registered on `create` and independent of `AGENT_TTY_HOME`. New `agent-tty home list [--all] [--json]` lists registered Homes — Active Homes by default, `--all` adds terminal-only ones — each with live active/total Session counts and a last-seen timestamp, newest first; `agent-tty home forget <path>` deregisters a Home without touching disk. The read-only `dashboard` gains an additive Home picker (press `H`, `Enter` to switch): browsing Homes performs a read-only scan that never reconciles or mutates a Session, while entering a Home reconciles exactly as the single-Home dashboard does today. Both surfaces prune dead or empty Homes on read so a deleted `mktemp -d` Home never lingers in a listing (ADR 0008, [#130](https://github.com/coder/agent-tty/issues/130)).

### Changed

- Both renderer backends (`libghostty-vt` and `ghostty-web`) now produce one canonical visible-screen form (exactly `rows` lines, full grapheme clusters, interior blank cells as spaces, ASCII-only trailing trim) shared by the new Screen Hash, host Screen Stability comparison, and the text Render Wait matcher. This narrows a long-standing divergence so the three can never disagree about "the screen", and intentionally changes the default `ghostty-web` stability/text-wait comparand on screens with grapheme clusters, interior gaps, or non-ASCII trailing characters ([#127](https://github.com/coder/agent-tty/pull/127)).
- README front door rewritten: agent-facing one-liner and "like Playwright, but for terminal apps" framing up top, a new **What you'd use it for** section, a **Watch sessions live** section covering the read-only `dashboard`, and explicit PNG + WebM artifact positioning vs text/asciicast tools. The command surface is folded into prose and moved after the demos; `ROADMAP.md` is retired and every cross-reference removed ([#122](https://github.com/coder/agent-tty/pull/122)). The Codex/Claude agent demo videos now sit right after **What you'd use it for**, before Quickstart, instead of being buried near the bottom ([#128](https://github.com/coder/agent-tty/pull/128)).
- **`gc` is now cross-Home by default** (backward-incompatible): plain `agent-tty gc` sweeps every registered Home and deregisters the ones it empties or finds deleted, rather than collecting only the default Home. The result envelope changes shape accordingly — a top-level `homes[]` of per-Home outcomes (`removedSessions`, `skippedSessions`, `totalBytesFreed`, `existed`, `deregistered`) plus aggregate `removedSessionCount`/`totalBytesFreed`/`deregisteredHomes` — replacing the former flat `removedSessions`/`skippedSessions`/`totalBytesFreed`. Pass `--home <path>` (or set `AGENT_TTY_HOME`) to scope collection to a single Home as before. gc never deletes a Home directory. Automation that relied on `gc` meaning the default Home, or on the old result shape, must pass `--home` and read `homes[]` (ADR 0008, [#130](https://github.com/coder/agent-tty/issues/130)).

## [v0.3.0] - 2026-06-03

Expand Down
26 changes: 24 additions & 2 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

## Language

**Home**:
An agent-tty state root: an absolute directory containing a `sessions/` tree (and optional config), identified entirely by its path and relocatable via `--home` or `AGENT_TTY_HOME`. It is effectively a per-root store of **Sessions**.
_Avoid_: session store, AGENT_TTY_HOME (that names the override variable, not the concept), OS home / `$HOME`

**Session**:
A long-lived PTY-backed terminal instance owned by `agent-tty`.

Expand Down Expand Up @@ -172,10 +176,20 @@ The post-merge process that creates and publishes the release tag from the defau
**Publish Pipeline**:
The tag-triggered automation that validates, packages, and publishes a release after the **Release Finalization Step**.

### Home registry and discovery

**Home Registry**:
A persisted, advisory, per-machine index of **Homes** that have hosted a **Session**, used to discover and pick **Homes** for observation. The **Home** directories remain the source of truth; the **Home Registry** is a reconciled cache that may go stale and is pruned, never an authority on which **Homes** exist.
_Avoid_: home database, home store, source of truth

**Active Home**:
A **Home** with at least one **Active Session**. It is the default scope of the **Home Registry** listing and the dashboard home picker, mirroring how `list` defaults to **Active Sessions**.
_Avoid_: live home, running home

### Dashboard and live observation

**Session Dashboard**:
The human-facing, read-only terminal surface that lists **Sessions** and presents the **Live View** of a selected **Session**. Its purpose is observation, not control.
The human-facing, read-only terminal surface that selects a **Home** from the **Home Registry**, lists that **Home**'s **Sessions**, and presents the **Live View** of a selected **Session**. Its purpose is observation, not control.
_Avoid_: viewer, attach UI

**Live View**:
Expand Down Expand Up @@ -210,6 +224,12 @@ _Avoid_: bare "agent", "Coder agent"

## Relationships

- A **Home** contains a set of **Sessions**; every **Session** belongs to exactly one **Home** and is located by that **Home**'s path.
- The **Home Registry** lists **Homes**; its default scope is **Active Homes**, and an `all` scope shows every registered **Home**, mirroring the `active`/`all` scope `list` and the **Session Dashboard** already apply to **Sessions**.
- A **Home** enters the **Home Registry** when it first hosts a **Session**, and is pruned once its directory or **Sessions** no longer exist; the registry is always reconciled against the **Home** directories, never the reverse.
- A **Home** directory is user-owned — its path is arbitrary via `--home`/`AGENT_TTY_HOME` — so agent-tty manages the **Sessions** inside a **Home** and that **Home**'s **Home Registry** entry, but never deletes the **Home** directory itself.
- Deregistering a **Home** has no effect on the **Home**; a deregistered **Home** re-enters the **Home Registry** the next time it hosts a **Session**. The **Home Registry** gates nothing.
- The **Home Registry** is per-machine: it indexes only **Homes** on the local machine, and sharing a **Home** across machines is out of scope because reconciliation and garbage collection assume local processes. (See the Home Registry ADR.)
- A **Session** has exactly one **Session Status** at a time.
- A `running` **Session** is **Active**, **Commandable**, and **Live Host Eligible**.
- An `exiting` **Session** is **Active** and **Live Host Eligible**, but not **Commandable**.
Expand All @@ -219,7 +239,8 @@ _Avoid_: bare "agent", "Coder agent"
- A **Session** has one **Event Log**.
- An **Offline Replay Eligible Session** is reconstructed from its persisted **Event Log** and manifest.
- A **Snapshot Result** is derived from exactly one **Semantic Snapshot**.
- A **Session Dashboard** presents a **Live View** of exactly one selected **Session** at a time.
- A **Session Dashboard** observes one **Home** at a time, chosen from the **Home Registry** (defaulting to the **Home** it was launched with); switching **Homes** is part of its read-only observation and sends no input.
- A **Session Dashboard** presents a **Live View** of exactly one selected **Session** at a time within the selected **Home**.
- A **Live View** reconstructs screen state from a **Session**'s **Event Log** and is never a **Command Target**.
- A **Live View** is produced by **Event Log Follow**, never by querying the live session host.
- **Event Log Follow** applies uniformly to **Live Host Eligible** and **Offline Replay Eligible** Sessions because it depends only on the append-only **Event Log**.
Expand Down Expand Up @@ -309,3 +330,4 @@ _Avoid_: bare "agent", "Coder agent"
- "demo" and "proof" are not interchangeable for coding-agent recordings: a **Hero Demo** optimizes for stable presentation, while a **Recursive Dogfood Proof** optimizes for self-dogfood coverage.
- "agent" is overloaded across four referents: this project's **Triage Agent** (a Claude Code instance), Coder's **Coder workspace agent** (the SSH/exec daemon), a generic AFK implementation agent (the actor on `ready-for-agent` issues — Phase 2 of the triage pipeline), and — in **Session Dashboard** product copy only — the external client driving a **Session** (often an AI coding agent). The last sense is deliberately **not** a domain term: the **Session Dashboard** and **Live View** are defined over **Sessions**, not agents, and the **Event Log** does not record which client sent input. Do not make the dashboard agent-aware (grouping or filtering by agent identity) without first extending the domain model. Always qualify in code comments and docs.
- "batch" is overloaded: a **Batch** (an ordered **Batch Step** sequence driven through one **Command Target** by the `batch` command) is unrelated to a **Triage Batch** (the set of issues processed by one **AFK Triage** invocation). They live in different subsystems; always rely on the qualifier.
- "home" is overloaded: an agent-tty **Home** (this project's state root — the `--home`/`AGENT_TTY_HOME` directory) is not the operating-system home directory (`$HOME`, `homedir()`), even though the default **Home** sits at `~/.agent-tty` inside it. Always qualify as "agent-tty Home" in prose and product copy whenever OS home is also in scope.
101 changes: 101 additions & 0 deletions docs/adr/0008-home-registry-advisory-per-machine.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
---
status: accepted
---

# Home Registry: an advisory, per-machine, gc-reconciled index of Homes

## Context

`agent-tty` manages **Sessions** under a **Home** — a state root selected by
`--home`/`AGENT_TTY_HOME` (default `~/.agent-tty`) that holds a `sessions/`
tree. `list` and the **Session Dashboard** operate on exactly one **Home**, the
one resolved for that invocation.

There is no way to enumerate **Homes**. The only cross-**Home** artifact is the
per-**Home** socket directory at `/tmp/agent-tty/<sha256(home)[:8]>/`
(`src/storage/sessionPaths.ts`), and that is a dead end for discovery: the name
is a one-way hash (the **Home** path cannot be recovered from it), the directory
is created but never removed when hosts die (`src/host/rpcServer.ts` only
unlinks the per-**Session** socket _file_), and `/tmp` is reboot-ephemeral.

We want the **Session Dashboard** to pick a **Home** and inspect that **Home**'s
**Sessions**, and we want a "which **Homes** am I using" listing comparable to
`list`. That requires persisting the set of **Homes**. Because the docs actively
encourage throwaway **Homes** (`AGENT_TTY_HOME="$(mktemp -d)"`), any persisted
set will accumulate dead entries as those directories are garbage-collected or
`rm -rf`'d.

## Decision

Introduce a **Home Registry**: a persisted, **advisory**, **per-machine** index
of **Homes** that have hosted a **Session**.

- **Location:** `${XDG_STATE_HOME:-~/.local/state}/agent-tty/homes.json`. The
path is a function of the OS user, never of `AGENT_TTY_HOME` — the registry
spans **Homes**, so it cannot live inside one (a **Home** is relocatable).
- **Entry shape:** `{ path, lastSeenAt }` only. All **Session** state (active
counts, statuses) is **derived live** by scanning the **Home** at read time,
never cached in the registry. `home list` sorts newest-`lastSeenAt`-first,
mirroring how **Sessions** sort newest-`createdAt`-first.
- **Source of truth is the Home directories.** The registry is reconciled
_against_ them, never the reverse. A **Home** auto-registers when it first
hosts a **Session** (on `create`). It is reconciled out by three layered
mechanisms: **prune-on-read** (listings and the dashboard picker hide **Homes**
whose directory or `sessions/` is gone), **per-Home gc deregistering** a
**Home** it empties, and a **cross-Home gc sweep**.
- **`gc` becomes cross-Home by default** — it sweeps the whole **Home
Registry** — with `--home` (or an explicit `AGENT_TTY_HOME`) scoping it to a
single **Home**. gc collects Collectable **Session** directories and prunes
registry entries, but **never deletes a Home directory** (the path is
user-owned and arbitrary). `home forget <path>` is a non-destructive manual
deregister.
- **CLI:** a new `home` command group — `home list [--all] [--json]` (**Active
Homes** by default, `--all` includes terminal-only **Homes**) and
`home forget <path>`.
- **Dashboard:** the **Session Dashboard** gains a read-only **Home** picker
(default scope **Active Homes**). Browsing **Homes** does **not** reconcile;
full reconcile happens only on **Home** entry, exactly as it already does for a
single **Home**.

## Consequences

- The picker and `home list` never show a stale **Home** (prune-on-read), and
the file stays small (gc sweep) without `agent-tty` ever deleting a user
directory.
- Deregistration is safe and idempotent: a deregistered **Home** re-registers
the next time it hosts a **Session**; the registry gates nothing.
- **Cross-machine sharing of a Home is unsupported.** Cross-**Home** gc
reconciles **Sessions** across every registered **Home**, and reconciliation
judges liveness with local PIDs (`isProcessAlive` = `process.kill(pid, 0)`)
and SIGKILLs dead-host orphans (`killProcessBestEffort` =
`process.kill(pid, 'SIGKILL')`, `src/host/lifecycle.ts`). Manifests carry no
machine identity (`src/protocol/schemas.ts` records only `hostPid`/`childPid`).
So a **Home** shared across machines (e.g. NFS) is a hazard: a cross-**Home**
gc on machine B could mark machine A's live **Session** `failed` and SIGKILL
an unrelated local PID. The per-machine, local-only boundary contains this and
matches the Coder model (separate workspaces are separate machines and
filesystems, each with its own registry).
- **Backward-incompatible CLI change:** plain `gc` changes from "collect the
default **Home**" to "sweep all registered **Homes**." Automation that relied
on `gc` meaning the default **Home** must pass `--home`.

## Alternatives considered

- **An authoritative registry** (add/remove are the only way **Homes** exist).
Rejected: it becomes a second source of truth that can disagree with the
filesystem (a `rm -rf`'d **Home**, or a **Home** created before this feature).
An advisory index reconciled at read time never diverges.
- **Deriving active Homes from the `/tmp` socket tree** instead of persisting.
Rejected: the directory name is a one-way hash (no path recovery), socket
directories linger after hosts die (false "active"), the tree is
reboot-ephemeral, and it could never surface terminal-only **Homes** for
offline replay.
- **A machine-identity guard now** (stamp manifests with a machine id; skip
reconcile/kill for **Sessions** whose id ≠ current). Deferred, not rejected: it
is the right hardening _if_ shared-filesystem or remote-aggregation **Homes**
ever come into scope, but it is unjustified for v1 given the per-machine
boundary and the Coder model. Tracked as follow-up.
- **gc deletes emptied Home directories.** Rejected: `--home` is an arbitrary,
user-owned path; a stale registry entry plus `rm -rf` is a footgun. gc stops
at **Session** directories and registry entries; the **Home** directory is the
user's to delete.
14 changes: 14 additions & 0 deletions src/cli/commands/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { sendRpc } from '../../host/rpcClient.js';
import { ERROR_CODES, makeCliError } from '../../protocol/errors.js';
import type { SessionRecord } from '../../protocol/schemas.js';
import { upsertHome } from '../../storage/homeRegistry.js';
import { readManifestIfExists } from '../../storage/manifests.js';
import {
manifestPath,
Expand Down Expand Up @@ -178,6 +179,19 @@ export async function runCreateCommand(options: CommandOptions): Promise<void> {
}

const home = options.context.home;
// Advisory: remember this Home in the per-machine Home Registry so it can be
// discovered later (`home list`, dashboard picker). A registry hiccup must
// never fail session creation — but log it at debug so it stays diagnosable
// instead of vanishing.
await upsertHome(home).catch((error: unknown) => {
options.context.logger.debug(
'failed to register Home in the Home Registry',
{
home,
error: error instanceof Error ? error.message : String(error),
},
);
});
const sessionDirectory = sessionDir(home, sessionId);
const socketFile = socketPath(sessionDirectory);
let lastError: CliError | null = null;
Expand Down
Loading
Loading