Repository navigation
feat(chat): chat-v1 host for Claude Code (adapter-based) - #65
Merged
Merged
Conversation
Approved design for the Atem host side of Astation's structured chat: transcript as the only content source, hooks for lifecycle, honest status, no approvals or resends from chat, client-neutral message model for web now and a mobile app later, and the additive contract changes for the Astation agent. 🤖 Built with SMT <smt@agora.build>
… + token Each Claude hook writes its event JSON to its own file in the private 0700 session directory (write then rename); Atem reads them in order and deletes them. No hidden subcommand, socket or token; same trust boundary. 🤖 Built with SMT <smt@agora.build>
🤖 Built with SMT <smt@agora.build>
🤖 Built with SMT <smt@agora.build>
…ug in) Agent-specific code (launch arguments, lifecycle source, log parser, capabilities) moves behind an AgentAdapter with neutral AgentEvent/LogItem types; the chat state, message model, wire, security and reconnects are shared. v1 ships the Claude Code adapter; agents without an adapter keep the shared terminal with status unknown and no chat-v1. 🤖 Built with SMT <smt@agora.build>
…adapter 🤖 Built with SMT <smt@agora.build>
🤖 Built with SMT <smt@agora.build>
…ude Code 2.1.299) 🤖 Built with SMT <smt@agora.build>
Items used only by later tasks are covered by one allow(dead_code) on mod chat in main.rs, to be removed in Task 6. 🤖 Built with SMT <smt@agora.build>
🤖 Built with SMT <smt@agora.build>
🤖 Built with SMT <smt@agora.build>
🤖 Built with SMT <smt@agora.build>
…ecks, history) 🤖 Built with SMT <smt@agora.build>
🤖 Built with SMT <smt@agora.build>
🤖 Built with SMT <smt@agora.build>
🤖 Built with SMT <smt@agora.build>
Each Claude spawn gets a chat directory, --settings hooks and a pinned --session-id (placed before the user's arguments, for that spawn only) and a new generation. poll_chat feeds hook events, then transcript items, then turn ends into the ChatSession each tick, times out unaccepted prompts, sends a 15 s status heartbeat and broadcasts to open conversations with per-conversation sequences. The agent registry shows Unknown until SessionStart, then the chat status, and chat-v1 while the adapter reports it; the agent list is re-sent on a change. agentChat open/submit/close is handled only on an authenticated link; disconnect and re-authentication close conversations. Process exit reports unknown and removes the chat directory. Codex registers as Unknown without chat. In-process end-to-end tests drive the App with a fake claude script in a PTY and a fake Astation peer; the launch binary test checks the hook arguments and that the chat directory is removed on quit. 🤖 Built with SMT <smt@agora.build>
🤖 Built with SMT <smt@agora.build>
- A Claude respawn first reports the old process's exit, then sends each open conversation `error` "the agent was restarted; reopen the chat" and `close`, then drops the old chat. - A fatal withdrawal sends `close` after the `error` (wire::close). - Opening a conversation restarts the 15 s heartbeat clock. - Launching Codex sends the agent list when connected. - Tests: wait for the expected history/status instead of fixed windows; offline and suspended links ignore `open` and suspend clears conversations; respawn, close-after-withdrawal, heartbeat reset and the Codex agent list are covered. - AGENTS.md and designs/agent-chat-host.md: late SessionStart still enables chat, close after withdrawal, restart row, `open` with `before` on an open conversation. 🤖 Built with SMT <smt@agora.build>
…l, prompt prefixes) - SessionStarted carries the hook's source and same_log; compaction or a resume on the same transcript keeps the turn, pending prompt and draft mark; startup, /clear or another transcript reset to ready. - Any user entry starting with "[Request interrupted by user" ends the turn as interrupted (also deny/Esc at a permission prompt). - Switching transcripts reads the old one to its end first (the switch waits one poll); a resumed session's existing content is backfill: history only, then open conversations get "the conversation was switched; reopen the chat" and close. - Chat prompts starting with "/" or "!" are refused. - Prompt matching trims both ends; a SessionStart whose transcript is not <session_id>.jsonl is ignored with a notice; chat dirs carry a pid file and stale ones are swept once per App; heartbeat per conversation, at most 8 conversations per agent; history capped at 8 MiB; live tests use a temp chat root. Spec and AGENTS.md updated. 🤖 Built with SMT <smt@agora.build>
The stale-dir sweep records the host next to the pid and never removes a directory another host owns (a shared home). Resuming a session that was already read does not add its prompts to history again. 🤖 Built with SMT <smt@agora.build>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Chat view of the Claude Code session running in Atem's shared terminal (
atem agent launch claude), for Astation's browser page (and a mobile app later). Design:designs/agent-chat-host.md; plan:designs/plans/2026-10-15-agent-chat-host.md.src/chat/adapter.rs): agent-specific code lives in one adapter; v1 ships Claude Code. Codex and others plug in later.src/chat/claude/): per-launch hooks via--settings(user~/.claudeuntouched) write one JSON file per event into a private 0700 directory;--session-idpins the transcript. Claude's transcript JSONL is the only content source. Tools are one-line notes; thinking, tool output and file contents are never sent.src/chat/session.rs, agent-neutral): status from real events (ready / working / waiting / unknown), onecompleteper turn, history, request-id dedupe, redaction throughmemory::secrets. A chat prompt is pasted (bracketed paste, all-or-nothing) only after six checks: generation, new request id, size and characters, ready, no terminal input since the last turn ended, no viewer holding control. Accepted only when both the hook and the transcript show it; otherwise "delivery unknown" after 10 s, never resent. Prompts starting with/or!are refused.src/chat/wire.rs):{"type":"agentChat","data":{"v":1,...}}on the authenticated Astation link, never during re-authentication.Unknownstatus, optionalcapabilities: ["chat-v1"](omitted when empty, so the agent-list bytes are unchanged for other agents). Codex is now listed, asUnknown.Known v1 limit (in the spec): an unfinished draft typed in the terminal during a turn is treated as consumed when the turn ends.
Testing
cargo test: 1265 unit tests (incl. 99 chat tests: transcript reader on recorded Claude Code 2.1.299/2.1.300 fixtures, hooks, session state machine, wire, in-process end-to-end with a fakeclaudeand a fake Astation peer) +agent_launch_binary(new: launch passes--settings/--session-id, chat dir removed on quit),key_agent_binary,terminal_ui_binary: all pass.cargo clippy --all-targets: no new warnings.Generated with SMT smt@agora.build