Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8ccef93
docs: agent chat (chat-v1) host design for Claude Code
guohai Oct 15, 2026
ca5cae5
docs(chat): hook events as one file each instead of a helper + socket…
guohai Oct 15, 2026
332b5ef
docs(chat): portable event-file names (mktemp) and ordering (mtime)
guohai Oct 15, 2026
e979c9a
docs(chat): hook command always exits 0 quietly
guohai Oct 15, 2026
c959f4a
docs(chat): adapter-based host design (Claude v1; Codex and others pl…
guohai Oct 15, 2026
dbdfd7c
docs(chat): units table belongs to the shared design, not the Claude …
guohai Oct 15, 2026
3747c1b
docs(chat): chat-v1 host implementation plan
guohai Oct 15, 2026
d4941da
test(chat): Claude Code hook and transcript fixtures (verified on Cla…
guohai Oct 15, 2026
4a29c3a
feat(chat): neutral adapter types and Claude transcript reader
guohai Oct 15, 2026
e11168c
fix(chat): transcript reader review fixes
guohai Oct 15, 2026
449b7c1
feat(chat): Claude hooks via event files and the Claude adapter
guohai Oct 15, 2026
a94d696
fix(chat): Claude adapter review fixes
guohai Oct 15, 2026
62a7af7
feat(chat): agent-neutral chat session (status, acceptance, submit ch…
guohai Oct 15, 2026
a45006b
fix(chat): chat session review fixes
guohai Oct 15, 2026
040083b
fix(chat): forget a timed-out request after the next prompt
guohai Oct 15, 2026
2a30545
feat(chat): agentChat v1 wire
guohai Oct 15, 2026
c349fb0
feat(chat): chat-v1 host wired into the TUI, Astation and agent registry
guohai Oct 15, 2026
71589b8
docs(chat): agent chat in AGENTS.md and READMEs
guohai Oct 15, 2026
0dd3744
fix(chat): review follow-ups for the chat host
guohai Oct 15, 2026
bcf6a2b
fix(chat): final review fixes (compaction, interrupts, resume backfil…
guohai Oct 15, 2026
fb5ab5f
fix(chat): keep another host's chat dirs; no repeated prompts on resume
guohai Oct 15, 2026
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
13 changes: 12 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ src/
├── app.rs # TUI state machine, mark task queue, Claude session management
├── app/terminal.rs # terminal-v1 message handling for the Claude/Codex sessions
├── app/reauth.rs # re-authentication when Astation sends auth_required over an open socket
├── app/chat.rs # chat-v1 host: adapter polling, conversations, submit, registry status/capabilities
├── app/chat_tests.rs # (test-only) chat-v1 end to end: the App, tests/fixtures/chat/fake-claude.sh in a PTY, a fake Astation
├── chat/ # Agent chat (chat-v1), agent-neutral except the adapters
│ ├── adapter.rs # AgentAdapter trait, AgentEvent, LogItem, Capabilities, LaunchPlan
│ ├── session.rs # per-agent chat state: status, history, draft check, submit/acceptance, request ids
│ ├── wire.rs # agentChat v1 envelopes to and from Astation
│ └── claude/ # Claude Code adapter: hooks.rs (chat dir, settings.json, event files), transcript.rs (session JSONL)
├── cli.rs # CLI command definitions
├── repl.rs # Interactive REPL mode
├── websocket_client.rs # Astation WebSocket protocol (message types + client)
Expand Down Expand Up @@ -144,6 +151,7 @@ designs/
├── voice-coding-stages.md # Voice coding implementation stages
├── remote-agent-control.md # Astation → atem → agent (text/voice/control keys, terminal-v1 proposal)
├── local-agent-runtime.md # Launch the original claude/codex CLI in Atem, local and remote access to one session
├── agent-chat-host.md # Agent chat (chat-v1): adapters, Claude hooks + transcript, status, wire
├── terminal-v1-implementation.md # Shared terminal: what Atem built, wire contract, keys, limits
├── atem-identity.md # instance_id + unique relay atem_id
├── vault.md # Shared cross-agent context store (relay + Postgres)
Expand Down Expand Up @@ -185,11 +193,14 @@ Key methods:
- `VisualizeRequest { session_id, topic, relay_url? }` - diagram request from Astation; diagram generation was removed, so atem answers each one at once with `VisualizeResult { success: false, message: "Diagram generation was removed from atem" }`
- `VisualizeResult { session_id, success, message, file_path? }` - sent back to Astation (always unsuccessful, no `file_path`)
- `AgentInput { agent_id?, kind, text?, key? }` - remote agent control: text or a control key from Astation → focused agent PTY (`handle_agent_input`)
- `AgentChat(ChatEnvelope)` - `{"type":"agentChat","data":{"v":1,"op":…}}` both ways: chat with a launched agent (`handle_agent_chat`, see Agent chat)
- Also: project lists, token requests, voice/video toggle, heartbeat, auth flow (`auth_required` → `auth`; the connect path and re-authentication share `auth_proof` to build the proof and `auth_response` to classify replies, so the two can't drift)

**Remote Agent Control** (`app.rs`, `websocket_client.rs`): Receives `AgentInput` from Astation and writes it to the focused agent's PTY — `kind:"text"` types a line and submits it (`\n\r`, matching `send_claude_prompt`); `kind:"key"` writes raw control bytes via `agent_key_to_bytes` (enter/esc/ctrl-c/arrows/y/n). Routes to the codex PTY when it's the active chat, else claude; no-ops if no agent session is live (v1 drives an already-running agent). Voice reuses the existing `VoiceRequest` path. While a terminal-v1 viewer is attached, `AgentInput` is refused (a status message says to use terminal-v1) and so is any `agent_id`. A socket alive is not an authenticated Astation: when Astation sends `auth_required` over an already-authenticated socket (e.g. it alone reconnected to the relay), `app/reauth.rs` shows `Astation ✗`, revokes terminal leases and ignores every other Astation request, then answers only the pinned Astation non-interactively (`AstationClient::answer_auth_required`) with the connect path's proof (`local_proof` on loopback, else the saved session); success restores online and re-sends the agent list (viewers subscribe again, view-only), while denial, expiry, a different `astation_id` or no answer within 15 s leaves a notice and closes the socket for the normal reconnect (never pairing).

**Shared terminal** (`src/terminal_share.rs`, `pty_session.rs`, `app/terminal.rs`, `tui/terminal_view.rs`): `atem agent launch [claude|claude-code|codex] [AGENT_ARGS]...` runs `tui::run_agent_tui`, which starts the installed CLI in a PTY (arguments are forwarded as separate values, with or without `--`; `CLAUDE_CLI_*`/`CODEX_CLI_*` env still apply and come first) and shows it full screen with one status row (`Atem | Claude|Codex | Local|Remote | Astation ✓|✗[ | Notice] | <prefix> then ?:help`, from `tui/draw.rs`: `Remote` while a viewer holds the control lease, `✗` whenever `astation_connected` is false — including during re-authentication — and `Notice` once `terminal_ui.messages` has an entry; prefix-pending, help, history and 4 s feedback replace it). It works without Astation. Over the authenticated Astation link, `{"type":"terminal","data":{"v":1,"op":...}}` messages (`list`, `subscribe`, `unsubscribe`, `ack`, `control`, `input`, `resize`, `stop` in; `sessions`, `attached`, `frame`, `controller`, `written`, `exited`, `error` out) let a viewer see the canonical screen as base64 ANSI frames (16 KiB per frame, 128 KiB unacknowledged, bounded queues) and, holding a control lease, send raw input bytes. Terminal ids are `claude` and `codex`; one Claude and one Codex session, one viewer each; sessions end when Atem quits. Local keys use the prefix Ctrl+] (`terminal_prefix` in config.toml or `ATEM_TERMINAL_PREFIX`, which wins, changes it; an invalid value falls back to Ctrl+] with a notice): `h`/`m` hide, `g` take control back, `x` stop, `r` redraw, `[` history, `c` copy, `?` help and recent notices. Background warnings go through `diagnostics` so they never write over the renderer. See `designs/terminal-v1-implementation.md` (the contract), `designs/local-agent-runtime.md` and `designs/remote-agent-control.md`.
**Shared terminal** (`src/terminal_share.rs`, `pty_session.rs`, `app/terminal.rs`, `tui/terminal_view.rs`): `atem agent launch [claude|claude-code|codex] [AGENT_ARGS]...` runs `tui::run_agent_tui`, which starts the installed CLI in a PTY (arguments are forwarded as separate values, with or without `--`; `CLAUDE_CLI_*`/`CODEX_CLI_*` env still apply and come first, after Claude's chat hooks) and shows it full screen with one status row (`Atem | Claude|Codex | Local|Remote | Astation ✓|✗[ | Notice] | <prefix> then ?:help`, from `tui/draw.rs`: `Remote` while a viewer holds the control lease, `✗` whenever `astation_connected` is false — including during re-authentication — and `Notice` once `terminal_ui.messages` has an entry; prefix-pending, help, history and 4 s feedback replace it). It works without Astation. Over the authenticated Astation link, `{"type":"terminal","data":{"v":1,"op":...}}` messages (`list`, `subscribe`, `unsubscribe`, `ack`, `control`, `input`, `resize`, `stop` in; `sessions`, `attached`, `frame`, `controller`, `written`, `exited`, `error` out) let a viewer see the canonical screen as base64 ANSI frames (16 KiB per frame, 128 KiB unacknowledged, bounded queues) and, holding a control lease, send raw input bytes. Terminal ids are `claude` and `codex`; one Claude and one Codex session, one viewer each; sessions end when Atem quits. Local keys use the prefix Ctrl+] (`terminal_prefix` in config.toml or `ATEM_TERMINAL_PREFIX`, which wins, changes it; an invalid value falls back to Ctrl+] with a notice): `h`/`m` hide, `g` take control back, `x` stop, `r` redraw, `[` history, `c` copy, `?` help and recent notices. Background warnings go through `diagnostics` so they never write over the renderer. See `designs/terminal-v1-implementation.md` (the contract), `designs/local-agent-runtime.md` and `designs/remote-agent-control.md`.

**Agent chat** (`src/chat/`, `app/chat.rs`): chat-v1 lets Astation show a launched agent as a chat. Only Claude Code has an adapter. Before each Claude spawn `prepare_claude_chat` creates `chat-<uuid>/` (0700) under `$XDG_RUNTIME_DIR/atem` (else `~/.config/atem/run`; `App.chat_root`) with `settings.json` (hooks for SessionStart, UserPromptSubmit, Pre/PostToolUse, Notification, Stop, each writing its stdin JSON to its own file in `events/`) and puts `--settings <file>` and, unless resuming, `--session-id <uuid>` before the user's arguments for that spawn only; each spawn is a new generation (UUID). It also writes a `pid` file; the first chat an App prepares sweeps `chat-*` directories whose pid is dead (or, without one, older than an hour). If preparing fails, Claude starts without chat. Every TUI tick `poll_chat` reads the event files (lifecycle only) and the transcript JSONL named by SessionStart (all message content), feeds `ChatSession` (log items before a tick's `Stop`, so `complete` follows the content), times out unaccepted prompts (10 s, never resent), re-sends status to each conversation 15 s after it last got it, and sends the results to each open conversation with its own sequence (at most 8 per agent: a ninth closes the oldest). A SessionStart whose transcript isn't `<session_id>.jsonl` is ignored with a notice; compaction or a resume on the same transcript keeps the turn, while startup, `/clear` or another transcript resets to ready (the old transcript is read to its end first, one tick later). Resuming another session reads its existing content as history only (no `message`/`complete`) and sends open conversations `error` "the conversation was switched; reopen the chat" and `close`. A user entry starting with `[Request interrupted by user` ends the turn as interrupted (Esc, or deny at a permission prompt). Status: ready/working/waiting/unknown, mapped in the agent registry to Idle/Thinking/WaitingForInput/Unknown; the registry entry is Unknown with no capabilities until SessionStart, then advertises `capabilities: ["chat-v1"]` while the adapter reports it, and the agent list is re-sent to Astation on a change. No SessionStart within 15 s leaves chat unavailable with a notice (a late one still enables it); a transcript format change withdraws chat (open conversations get `error` and `close`); the terminal is unaffected either way. `submit` is pasted as one bracketed paste plus Enter (`PtyInput::send_paste`, not counted as typing) only for the current generation, a new `requestId`, a clean prompt (≤ 32 768 bytes, no control characters except `\n`/`\t`, not starting with `/` or `!`: commands and shell mode are refused), status ready with nothing pending, no typing since the last turn ended (`PtyInput::input_count`; typing during a turn counts as consumed when it ends, so a mid-turn draft is not detected) and no terminal viewer in control; accepted when both the UserPromptSubmit hook and the transcript's user entry arrive. Approvals stay in the terminal. Outgoing text goes through the credential redactor. Conversations are handled only while `astation_connected` (not during re-authentication) and close on disconnect or re-authentication; a new `open` restores status and history (50 messages / 256 KiB, `before` pages). Claude exiting reports `unknown` and `error`, drops the adapter (removing its directory) and keeps history readable (500 messages / 8 MiB); a respawn sends the old conversations `error` "the agent was restarted; reopen the chat" and `close`. Codex is registered with status Unknown and no capabilities (the agent list is sent when it starts). See `designs/agent-chat-host.md`.

**Vault** (`vault_client.rs`): Client for the relay-hosted shared cross-agent context store. `atem vault new/list/read/write/set-summary` — a versioned, append-only store that multiple atems read/write to hand off context between their agents. Pure request builders + human/plain renderers + a thin reqwest executor (auth: `Authorization: session <id>` + `?id=<instance_id>`). The `/api/vault` endpoints + Postgres live in the relay-server (Astation repo). See `designs/vault.md`.

Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,15 @@ Ctrl+<letter> other than i or m are accepted. The `CLAUDE_CLI_BIN`/`CLAUDE_CLI_A
and `CODEX_CLI_BIN`/`CODEX_CLI_ARGS` variables still override the command.
See [`designs/terminal-v1-implementation.md`](designs/terminal-v1-implementation.md).

**Chat from Astation.** Astation can open a chat view of a Claude session
launched with `atem agent launch claude`: your prompts appear as messages and
Claude's replies come from its own session log, with a status of ready,
working, waiting or unknown. Atem adds its hooks with `--settings` for that run
only; your `~/.claude` settings are not changed. Prompts from chat are refused
while you have unsent input in the terminal, and approvals stay in the
terminal. Codex has no chat yet (its status is unknown). See
[`designs/agent-chat-host.md`](designs/agent-chat-host.md).

### Vault (shared cross-agent context)

```bash
Expand Down
Loading