repos / zmx

session persistence for terminal processes
git clone https://github.com/neurosnap/zmx.git

commit
d4fdb99
parent
eaf6c5a
author
Eric Bower
date
2025-10-14 09:11:54 -0400 EDT
docs: fmt
9 files changed,  +175, -64
M AGENTS.md
+76, -1
 1@@ -2,7 +2,7 @@
 2 
 3 The goal of this project is to create a way to attach and detach terminal sessions without killing the underlying linux process.
 4 
 5-When researching `zmx`, also read the README.md in the root of this project directory to learn more about the features, documentation, prior art, etc.
 6+When researching `zmx`, also read the @README.md in the root of this project directory to learn more about the features, documentation, prior art, etc.
 7 
 8 ## tech stack
 9 
10@@ -48,3 +48,78 @@ To inspect the source code for zig's standard library, look inside the `zig_std_
11 ## find ghostty library source code
12 
13 To inspect the source code for zig's standard library, look inside the `ghostty_src` folder.
14+
15+## Issue Tracking
16+
17+We use bd (beads, https://github.com/steveyegge/beads) for issue tracking instead of Markdown TODOs or external tools.
18+
19+### Quick Reference
20+
21+```bash
22+# Find ready work (no blockers)
23+bd ready --json
24+
25+# Create new issue
26+bd create "Issue title" -t bug|feature|task -p 0-4 -d "Description" --json
27+
28+# Create with explicit ID (for parallel workers)
29+bd create "Issue title" --id worker1-100 -p 1 --json
30+
31+# Update issue status
32+bd update <id> --status in_progress --json
33+
34+# Link discovered work (old way)
35+bd dep add <discovered-id> <parent-id> --type discovered-from
36+
37+# Create and link in one command (new way)
38+bd create "Issue title" -t bug -p 1 --deps discovered-from:<parent-id> --json
39+
40+# Complete work
41+bd close <id> --reason "Done" --json
42+
43+# Show dependency tree
44+bd dep tree <id>
45+
46+# Get issue details
47+bd show <id> --json
48+
49+# Import with collision detection
50+bd import -i .beads/issues.jsonl --dry-run             # Preview only
51+bd import -i .beads/issues.jsonl --resolve-collisions  # Auto-resolve
52+```
53+
54+### Workflow
55+
56+1. **Check for ready work**: Run `bd ready` to see what's unblocked
57+1. **Claim your task**: `bd update <id> --status in_progress`
58+1. **Work on it**: Implement, test, document
59+1. **Discover new work**: If you find bugs or TODOs, create issues:
60+   - Old way (two commands): `bd create "Found bug in auth" -t bug -p 1 --json` then `bd dep add <new-id> <current-id> --type discovered-from`
61+   - New way (one command): `bd create "Found bug in auth" -t bug -p 1 --deps discovered-from:<current-id> --json`
62+1. **Complete**: `bd close <id> --reason "Implemented"`
63+1. **Export**: Changes auto-sync to `.beads/issues.jsonl` (5-second debounce)
64+
65+### Issue Types
66+
67+- `bug` - Something broken that needs fixing
68+- `feature` - New functionality
69+- `task` - Work item (tests, docs, refactoring)
70+- `epic` - Large feature composed of multiple issues
71+- `chore` - Maintenance work (dependencies, tooling)
72+
73+### Priorities
74+
75+- `0` - Critical (security, data loss, broken builds)
76+- `1` - High (major features, important bugs)
77+- `2` - Medium (nice-to-have features, minor bugs)
78+- `3` - Low (polish, optimization)
79+- `4` - Backlog (future ideas)
80+
81+### Dependency Types
82+
83+- `blocks` - Hard dependency (issue X blocks issue Y)
84+- `related` - Soft relationship (issues are connected)
85+- `parent-child` - Epic/subtask relationship
86+- `discovered-from` - Track issues discovered during work
87+
88+Only `blocks` dependencies affect the ready work queue.
M README.md
+5, -5
 1@@ -14,11 +14,11 @@
 2 - The `daemon` is managed by a supervisor like `systemd`
 3 - We provide a `systemd` unit file that users can install that manages the `daemon` process
 4 - The cli tool supports the following commands:
 5-    - `attach {session}`: attach to the pty process
 6-    - `detach`: detach from the pty process without killing it
 7-    - `kill {session}`: kill the pty process
 8-    - `list`: show all sessions and what clients are currently attached
 9-    - `daemon`: the background process that manages all sessions
10+  - `attach {session}`: attach to the pty process
11+  - `detach`: detach from the pty process without killing it
12+  - `kill {session}`: kill the pty process
13+  - `list`: show all sessions and what clients are currently attached
14+  - `daemon`: the background process that manages all sessions
15 - This project does **NOT** provide windows, tabs, or window splits
16 - It supports all the terminal features that the client's terminal emulator supports
17 - The current version only works on linux
M plans/cli.md
+5, -5
 1@@ -15,11 +15,11 @@ This document outlines the plan for implementing the CLI scaffolding for the `zm
 2 
 3 - Use `zig-clap` to define the command structure specified in `specs/cli.md`.
 4 - This includes the global options (`-h`, `-v`) and the subcommands:
 5-    - `daemon`
 6-    - `list`
 7-    - `attach <session>`
 8-    - `detach <session>`
 9-    - `kill <session>`
10+  - `daemon`
11+  - `list`
12+  - `attach <session>`
13+  - `detach <session>`
14+  - `kill <session>`
15 - For each command, define the expected arguments and options.
16 
17 ## 4. Integrate with `src/main.zig`
M plans/daemon.md
+13, -13
 1@@ -15,11 +15,11 @@ This document outlines the plan for implementing the `zmx daemon` subcommand, ba
 2 ## 3. Implement Session Management
 3 
 4 - Define a `Session` struct to manage the state of each PTY process. This struct will include:
 5-    - The session name.
 6-    - The file descriptor for the PTY.
 7-    - A buffer for the terminal output (scrollback).
 8-    - The terminal state, managed by `libghostty-vt`.
 9-    - A list of connected client IDs.
10+  - The session name.
11+  - The file descriptor for the PTY.
12+  - A buffer for the terminal output (scrollback).
13+  - The terminal state, managed by `libghostty-vt`.
14+  - A list of connected client IDs.
15 - Use a `std.StringHashMap(Session)` to store and manage all active sessions.
16 
17 ## 4. Implement PTY Management
18@@ -31,19 +31,19 @@ This document outlines the plan for implementing the `zmx daemon` subcommand, ba
19 ## 5. Implement the Main Event Loop
20 
21 - The core of the daemon will be an event loop (using `libxev` on Linux) that concurrently handles:
22-    1.  New client connections on the main Unix socket.
23-    2.  Incoming requests from connected clients.
24-    3.  Output from the PTY processes.
25+  1. New client connections on the main Unix socket.
26+  1. Incoming requests from connected clients.
27+  1. Output from the PTY processes.
28 - This will allow the daemon to be single-threaded and highly concurrent.
29 
30 ## 6. Implement Protocol Handlers
31 
32 - For each message type defined in `protocol.md`, create a handler function:
33-    - `handle_list_sessions_request`: Responds with a list of all active sessions.
34-    - `handle_attach_session_request`: Adds the client to the session's list of connected clients and sends them the scrollback buffer.
35-    - `handle_detach_session_request`: Removes the client from the session's list.
36-    - `handle_kill_session_request`: Terminates the PTY process and removes the session.
37-    - `handle_pty_input`: Writes the received data to the corresponding PTY.
38+  - `handle_list_sessions_request`: Responds with a list of all active sessions.
39+  - `handle_attach_session_request`: Adds the client to the session's list of connected clients and sends them the scrollback buffer.
40+  - `handle_detach_session_request`: Removes the client from the session's list.
41+  - `handle_kill_session_request`: Terminates the PTY process and removes the session.
42+  - `handle_pty_input`: Writes the received data to the corresponding PTY.
43 - When there is output from a PTY, the daemon will create a `pty_output` message and send it to all attached clients.
44 
45 ## 7. Integrate with `main.zig`
M plans/session-restore.md
+24, -14
  1@@ -5,9 +5,10 @@ This document outlines the plan for implementing session restore functionality i
  2 ## Overview
  3 
  4 When a client detaches and later reattaches to a session, we need to restore the terminal to its exact visual state without replaying all historical output. We achieve this by:
  5+
  6 1. Parsing all PTY output through libghostty-vt to maintain an up-to-date terminal grid
  7-2. Proxying raw bytes to attached clients (no latency impact)
  8-3. Rendering the terminal grid to ANSI on reattach
  9+1. Proxying raw bytes to attached clients (no latency impact)
 10+1. Rendering the terminal grid to ANSI on reattach
 11 
 12 ## 1. Add libghostty-vt Dependency
 13 
 14@@ -18,6 +19,7 @@ When a client detaches and later reattaches to a session, we need to restore the
 15 ## 2. Extend the Session Struct
 16 
 17 Add to `Session` struct in `daemon.zig`:
 18+
 19 ```zig
 20 const Session = struct {
 21     name: []const u8,
 22@@ -38,6 +40,7 @@ const Session = struct {
 23 ## 3. Initialize Terminal Emulator on Session Creation
 24 
 25 In `createSession()`:
 26+
 27 - After forking PTY, initialize libghostty-vt instance
 28 - Configure terminal size (rows, cols) - query from PTY or use defaults (e.g., 24x80)
 29 - Configure scrollback buffer size (make this configurable, default 10,000 lines)
 30@@ -64,6 +67,7 @@ fn createSession(allocator: std.mem.Allocator, session_name: []const u8) !*Sessi
 31 ## 4. Parse PTY Output Through Terminal Emulator
 32 
 33 Modify `readPtyCallback()`:
 34+
 35 - Feed all PTY output bytes to libghostty-vt first
 36 - Check if there are attached clients
 37 - If clients attached: proxy raw bytes directly to them (existing behavior)
 38@@ -101,6 +105,7 @@ fn readPtyCallback(...) xev.CallbackAction {
 39 ## 5. Render Terminal State on Reattach
 40 
 41 Create new function `renderTerminalSnapshot()`:
 42+
 43 - Get current grid from libghostty-vt
 44 - Serialize grid to ANSI escape sequences
 45 - Send rendered output to reattaching client
 46@@ -151,12 +156,13 @@ fn renderTerminalSnapshot(session: *Session, allocator: std.mem.Allocator) ![]u8
 47 ## 6. Modify handleAttachSession()
 48 
 49 Update attach logic to:
 50+
 51 1. Check if session exists, create if not
 52-2. If reattaching (session already exists):
 53+1. If reattaching (session already exists):
 54    - Render current terminal state using libghostty-vt
 55    - Send rendered snapshot to client
 56-3. Add client to session's attached_clients set
 57-4. Start proxying raw PTY output
 58+1. Add client to session's attached_clients set
 59+1. Start proxying raw PTY output
 60 
 61 ```zig
 62 fn handleAttachSession(ctx: *ServerContext, client: *Client, session_name: []const u8) !void {
 63@@ -203,6 +209,7 @@ fn handleAttachSession(ctx: *ServerContext, client: *Client, session_name: []con
 64 ## 7. Handle Window Resize Events
 65 
 66 Add support for window size changes:
 67+
 68 - When client sends window resize event, update libghostty-vt
 69 - Update PTY window size with ioctl TIOCSWINSZ
 70 - libghostty-vt will handle reflow automatically
 71@@ -229,6 +236,7 @@ fn handleWindowResize(client: *Client, rows: u16, cols: u16) !void {
 72 ## 8. Track Attached Clients Per Session
 73 
 74 Modify session management:
 75+
 76 - Remove client from session.attached_clients on detach
 77 - On disconnect, automatically detach client
 78 - Keep session alive even when no clients attached
 79@@ -248,6 +256,7 @@ fn handleDetachSession(client: *Client, session_name: []const u8, target_client_
 80 ## 9. Clean Up Terminal Emulator on Session Destroy
 81 
 82 In session deinit:
 83+
 84 - Free libghostty-vt resources
 85 - Clean up attached_clients map
 86 
 87@@ -265,6 +274,7 @@ fn deinit(self: *Session) void {
 88 ## 10. Configuration Options
 89 
 90 Add configurable options (future work):
 91+
 92 - Scrollback buffer size
 93 - Default terminal dimensions
 94 - Maximum grid memory usage
 95@@ -272,15 +282,15 @@ Add configurable options (future work):
 96 ## Implementation Order
 97 
 98 1. ✅ Add libghostty-vt C bindings and build integration
 99-2. ✅ Extend Session struct with vt fields
100-3. ✅ Initialize vt in createSession()
101-4. ✅ Feed PTY output to vt in readPtyCallback()
102-5. ✅ Implement renderTerminalSnapshot()
103-6. ✅ Modify handleAttachSession() to render on reattach
104-7. ✅ Track attached_clients per session
105-8. ✅ Handle window resize events
106-9. ✅ Clean up vt resources in session deinit
107-10. ✅ Test with multiple attach/detach cycles
108+1. ✅ Extend Session struct with vt fields
109+1. ✅ Initialize vt in createSession()
110+1. ✅ Feed PTY output to vt in readPtyCallback()
111+1. ✅ Implement renderTerminalSnapshot()
112+1. ✅ Modify handleAttachSession() to render on reattach
113+1. ✅ Track attached_clients per session
114+1. ✅ Handle window resize events
115+1. ✅ Clean up vt resources in session deinit
116+1. ✅ Test with multiple attach/detach cycles
117 
118 ## Testing Strategy
119 
M specs/cli.md
+3, -3
 1@@ -54,7 +54,7 @@ The `list` command will output a table with the following columns:
 2 - `CLIENTS`: The number of clients currently attached to the session.
 3 - `CREATED_AT`: The date when the session was created
 4 
 5----
 6+______________________________________________________________________
 7 
 8 #### `attach`
 9 
10@@ -71,7 +71,7 @@ zmx attach <session>
11 - `<session>`: The name of the session to attach to. This is a required argument.
12 - `<socket>`: The location of the unix socket file.
13 
14----
15+______________________________________________________________________
16 
17 #### `detach`
18 
19@@ -88,7 +88,7 @@ zmx detach <session>
20 - `<session>`: The name of the session to detach from. This is a required argument.
21 - `<socket>`: The location of the unix socket file.
22 
23----
24+______________________________________________________________________
25 
26 #### `kill`
27 
M specs/daemon.md
+5, -5
 1@@ -10,11 +10,11 @@ The `zmx daemon` subcommand starts the long-running background process that mana
 2 
 3 The daemon is responsible for:
 4 
 5-1.  **PTY Management**: Creating, managing, and destroying PTY processes using `fork` or `forkpty`.
 6-2.  **Session State Management**: Maintaining the terminal state and a buffer of text output for each active session. This ensures that when a client re-attaches, they see the previous output and the correct terminal state.
 7-3.  **Client Communication**: Facilitating communication between multiple `zmx` client instances and the managed PTY processes via a Unix socket.
 8-4.  **Session Lifecycle**: Handling the lifecycle of sessions, including creation, listing, attachment, detachment, and termination (killing).
 9-5.  **Resource Management**: Managing system resources associated with each session.
10+1. **PTY Management**: Creating, managing, and destroying PTY processes using `fork` or `forkpty`.
11+1. **Session State Management**: Maintaining the terminal state and a buffer of text output for each active session. This ensures that when a client re-attaches, they see the previous output and the correct terminal state.
12+1. **Client Communication**: Facilitating communication between multiple `zmx` client instances and the managed PTY processes via a Unix socket.
13+1. **Session Lifecycle**: Handling the lifecycle of sessions, including creation, listing, attachment, detachment, and termination (killing).
14+1. **Resource Management**: Managing system resources associated with each session.
15 
16 ## usage
17 
M specs/protocol.md
+42, -15
  1@@ -13,6 +13,7 @@ All messages are currently serialized using **newline-delimited JSON (NDJSON)**.
  2 ### Implementation
  3 
  4 The protocol implementation is centralized in `src/protocol.zig`, which provides:
  5+
  6 - Typed message structs for all payloads
  7 - `MessageType` enum for type-safe dispatching
  8 - Helper functions: `writeJson()`, `parseMessage()`, `parseMessageType()`
  9@@ -23,15 +24,18 @@ The protocol implementation is centralized in `src/protocol.zig`, which provides
 10 The protocol uses a hybrid approach: JSON for control messages and binary frames for PTY output to avoid encoding overhead and improve throughput.
 11 
 12 **Frame Format:**
 13+
 14 ```
 15 [4-byte length (little-endian)][2-byte type (little-endian)][payload...]
 16 ```
 17 
 18 **Frame Types:**
 19+
 20 - Type 1 (`json_control`): JSON control messages (not currently used in framing)
 21 - Type 2 (`pty_binary`): Raw PTY output bytes
 22 
 23 **Current Usage:**
 24+
 25 - Control messages (attach, detach, kill, etc.): NDJSON format
 26 - PTY output from daemon to client: Binary frames (type 2)
 27 - PTY input from client to daemon: Binary frames (type 2)
 28@@ -56,14 +60,19 @@ Responses are sent from the daemon to the client in response to a request. Every
 29 ### `list_sessions`
 30 
 31 - **Direction**: Client -> Daemon
 32+
 33 - **Request Type**: `list_sessions_request`
 34+
 35 - **Request Payload**: (empty)
 36 
 37 - **Direction**: Daemon -> Client
 38+
 39 - **Response Type**: `list_sessions_response`
 40+
 41 - **Response Payload**:
 42-    - `status`: `ok`
 43-    - `sessions`: An array of session objects.
 44+
 45+  - `status`: `ok`
 46+  - `sessions`: An array of session objects.
 47 
 48 **Session Object:**
 49 
 50@@ -75,43 +84,61 @@ Responses are sent from the daemon to the client in response to a request. Every
 51 ### `attach_session`
 52 
 53 - **Direction**: Client -> Daemon
 54+
 55 - **Request Type**: `attach_session_request`
 56+
 57 - **Request Payload**:
 58-    - `session_name`: string
 59-    - `rows`: u16 (terminal height in rows)
 60-    - `cols`: u16 (terminal width in columns)
 61+
 62+  - `session_name`: string
 63+  - `rows`: u16 (terminal height in rows)
 64+  - `cols`: u16 (terminal width in columns)
 65 
 66 - **Direction**: Daemon -> Client
 67+
 68 - **Response Type**: `attach_session_response`
 69+
 70 - **Response Payload**:
 71-    - `status`: `ok` or `error`
 72-    - `error_message`: string (if status is `error`)
 73+
 74+  - `status`: `ok` or `error`
 75+  - `error_message`: string (if status is `error`)
 76 
 77 ### `detach_session`
 78 
 79 - **Direction**: Client -> Daemon
 80+
 81 - **Request Type**: `detach_session_request`
 82+
 83 - **Request Payload**:
 84-    - `session_name`: string
 85+
 86+  - `session_name`: string
 87 
 88 - **Direction**: Daemon -> Client
 89+
 90 - **Response Type**: `detach_session_response`
 91+
 92 - **Response Payload**:
 93-    - `status`: `ok` or `error`
 94-    - `error_message`: string (if status is `error`)
 95+
 96+  - `status`: `ok` or `error`
 97+  - `error_message`: string (if status is `error`)
 98 
 99 ### `kill_session`
100 
101 - **Direction**: Client -> Daemon
102+
103 - **Request Type**: `kill_session_request`
104+
105 - **Request Payload**:
106-    - `session_name`: string
107+
108+  - `session_name`: string
109 
110 - **Direction**: Daemon -> Client
111+
112 - **Response Type**: `kill_session_response`
113+
114 - **Response Payload**:
115-    - `status`: `ok` or `error`
116-    - `error_message`: string (if status is `error`)
117+
118+  - `status`: `ok` or `error`
119+  - `error_message`: string (if status is `error`)
120 
121 ### `pty_in`
122 
123@@ -119,7 +146,7 @@ Responses are sent from the daemon to the client in response to a request. Every
124 - **Message Type**: `pty_in`
125 - **Format**: NDJSON
126 - **Payload**:
127-    - `text`: string (raw UTF-8 text from terminal input)
128+  - `text`: string (raw UTF-8 text from terminal input)
129 
130 This message is sent when a client wants to send user input to the PTY. It is a fire-and-forget message with no direct response. The input is forwarded to the shell running in the session's PTY.
131 
132@@ -129,7 +156,7 @@ This message is sent when a client wants to send user input to the PTY. It is a
133 - **Message Type**: `pty_out`
134 - **Format**: NDJSON (used only for control sequences like screen clear)
135 - **Payload**:
136-    - `text`: string (escape sequences or control output)
137+  - `text`: string (escape sequences or control output)
138 
139 This JSON message is sent for special control output like initial screen clearing. Regular PTY output uses binary frames (see below).
140 
M specs/restore-session.md
+2, -3
 1@@ -4,7 +4,7 @@ This document outlines the specification how we are going to preserve session st
 2 
 3 ## purpose
 4 
 5-The `zmx attach` subcommand starts re-attaches to a previously created session.  When doing this we want to restore the session to its current state, displaying the last working screen text, layout, text wrapping, etc.  This will include a configurable scrollback buffer size that will also be restored upon reattach.
 6+The `zmx attach` subcommand starts re-attaches to a previously created session. When doing this we want to restore the session to its current state, displaying the last working screen text, layout, text wrapping, etc. This will include a configurable scrollback buffer size that will also be restored upon reattach.
 7 
 8 ## technical details
 9 
10@@ -15,8 +15,7 @@ The `zmx attach` subcommand starts re-attaches to a previously created session.
11 - When you reattach, the daemon does not send the historic byte stream; instead it renders the current grid into a fresh ANSI sequence and ships that down the Unix-domain socket to the new shpool attach client.
12 - The client simply write()s that sequence to stdout—your local terminal sees it and redraws the screen instantly.
13 
14-So the emulator is not “between” client and daemon in the latency sense; it is alongside, maintaining state.
15-The only time it interposes is on re-attach: it briefly synthesizes a single frame so your local terminal can show the exact session image without having to replay minutes or hours of output.
16+So the emulator is not “between” client and daemon in the latency sense; it is alongside, maintaining state. The only time it interposes is on re-attach: it briefly synthesizes a single frame so your local terminal can show the exact session image without having to replay minutes or hours of output.
17 
18 ## using libghostty-vt
19