8000 feat: support agent-backed tmux sessions by bu5hm4nn · Pull Request #1 · bu5hm4nn/zedra · GitHub
[go: up one dir, main page]

Skip to content

feat: support agent-backed tmux sessions - #1

Draft
bu5hm4nn wants to merge 18 commits into
mainfrom
feat/tmux-agent-sessions
Draft

bu5hm4nn wants to merge 18 commits into
mainfrom
feat/tmux-agent-sessions

Conversation

@bu5hm4nn
@bu5hm4nn bu5hm4nn commented Sep 8, 2026 •
Copy link
Copy Markdown
Owner

Summary

  • discover, resume, attach, and terminate agent-backed tmux sessions
  • render custom tmux sessions with historical metadata and coherent single-agent filtering
  • show deduplicated phone, tablet, desktop, and unmanaged tmux client presence

Notes

  • protocol, session, host, and app unit suites pass; iOS host and cross-target checks pass
  • Android packaging remains unverified locally because cargo-ndk is not installed
  • the combined host integration run still hits the existing iroh connection reset in test_full_rpc_over_iroh

## Summary
- Add `zedra-host::tmux` with the owned `zedra-pi-<hex>` session-name codec: reversible, collision-free, safe as a tmux target; empty IDs refused, every foreign or malformed name decodes to untracked.
- Add pure parsers for the proven tmux 3.3a two-call `list-panes` `|`-formats (exact 4-field process records; positional metadata records with pipe-tolerant titles), explicit dead/exit-status representation, and `tmux -V` version parsing with a 3.3a minimum.
- Unit tests cover UUID/whitespace/quote/metacharacter/Unicode IDs, malformed hex, foreign names, version boundaries, malformed records, dead panes, round-trip stability, and classification precedence.

## Notes
- tmux 3.3a sanitizes all format output, so the plan's non-printing field separator is replaced by the proven two-call `|`-format recorded in the todo deltaqdev#2 proof findings.
- Pure codec and parsers only; subprocess operations, live capability checks, and attach/kill flows follow in the next commit.
## Summary
- Concrete TmuxClient: PATH discovery with version gating (>= 3.3a),
  private-socket argv builder, and a 5s subprocess deadline on every call.
- prepare_session runs new-session -d -A headless with the workdir and the
  authoritative resume command, tolerates the proven already-prepared
  race response plus one fresh-socket connect retry, then sets
  session-scoped mouse on and window-size largest.
- list_sessions returns only Zedra-owned sessions with their first pane's
  process + metadata via the proven two-call list-panes formats; foreign
  and malformed names are omitted; both proven no-server stderr variants
  map to an empty list instead of an error.
- terminate_session rebuilds the target from (slug, session_id), refuses
  foreign slugs, empty IDs, and non-owned targets, and treats an
  already-vanished session as terminated.
- attach_command returns "exec <tmux> attach-session -t <name> || exit $?"
  with shell quoting so a failed attach cannot fall back to a login shell.

## Notes
- Unit tests for the operations land in the following test(host) commit
  to keep this commit within the 400-line boundary; lifecycle tests with
  a real private-socket tmux are todo deltaqdev#9.
- Subprocess helper reuses agent::utils::command_output_with_timeout.
## Summary
- Exact argv assertions for list/prepare/set-option/kill paths, including
  private-socket injection, on top of the TmuxClient from the previous
  commit.
- Shell-quoting coverage for attach_command: spaced binary paths, quoted
  socket names, and quote-bearing session names.
- Proven no-server stderr variants classify as available capability;
  a missing binary surfaces as an error instead of an empty list.
- Owned-only listing filter, terminate validation for foreign slugs and
  empty IDs, and empty-ID rejection before any subprocess spawn.

## Notes
- All tests are pure and never touch a real tmux server; the ignored
  private-socket lifecycle suite is todo deltaqdev#9.
## Summary

Append `AgentShareList` and `AgentShareTerminate` request variants at the
`ZedraProto` tail with all-new wire types (`AgentShareListReq`,
`AgentShareSession`, `AgentShareListResult`, `AgentShareTerminateReq`,
`AgentShareTerminateResult`). No ALPN bump, no change to existing wire types
or `proto_v3.rs`, following the `HostDirList`/`HostWorkspaceOpen` append-only
rule.

Add the `AgentActor::supports_shared_sessions()` capability (false default,
true override only in `PiActor`) and gate new `rpc_daemon.rs` dispatch through
it — no Pi-specific match arms. List resolves tmux availability through
`TmuxClient`; missing/too-old tmux returns `available: false` with the exact
actionable reason while persisted history is unaffected. Terminate validates
slug + session id through the actor registry and tmux ownership codec before
killing the tmux session; `terminal_ids` stays empty until terminals carry
shared spawn metadata.

Add per-feature `AtomicBool` downgrade state to `SessionHandle`
(`shared_agent_sessions_supported`) with `agent_share_list` /
`agent_share_terminate`; an incompatible old host disables only these new
calls for the connection.

Document the request/result contract in `docs/PROTOCOL_SPECS.md` (§5.8 +
changelog).

## Notes

Protocol round-trip tests and downgrade tests follow in the next commit to
keep each change within the 400-line budget. Resume routing, `TermSession`
spawn metadata, and app UI are later todos.
## Summary

Protocol round-trip tests for the new `AgentShare*` wire types and enum
variants (including the unavailable-tmux and error payload shapes), and
`SessionHandle` downgrade tests: an incompatible old host disables only the
shared-session calls, unrelated capabilities stay enabled for the same
connection, transport errors do not downgrade, and disabled calls fail fast
without touching the wire.

## Notes

Plan-sanctioned test split from `feat(rpc): expose shared agent sessions` to
stay within the 400-line per-commit budget; both commits build and pass their
narrow checks independently.
## Summary

- Add host-internal spawn metadata: `SpawnOptions.identity_launch_cmd` plus
  `shared: Option<SharedSpawnIdentity>` carried onto `TermSession`;
  `initial_host_meta` reports the identity command (the original Pi resume
  command) so terminal cards never see the outer tmux attach wrapper.
- Route both resume entry points (`ZedraMessage::AgentResume` and REST
  `POST /v1/agents/{slug}/sessions/{id}/resume`) through one helper,
  `agent::resume_terminal_launch`: actors without the shared-session
  capability keep the exact direct launch; Pi gets
  `new-session -d -A` prepared before any terminal is created and returns
  the attach command plus the original Pi command as identity metadata.
- Prepare-before-insert: missing/old/hung tmux, invalid ids, timeouts, and
  option failures surface as an actionable resume error with no
  `TermSession` inserted and never a direct-Pi fallback; a PTY allocation
  failure after atomic creation leaves the shared session discoverable.
- Explicit share termination kills the validated tmux session first and
  only then removes every matching `TermSession` from the current
  `ServerSession`, reaping those client children through the normal close
  path and returning their terminal ids; a failed kill keeps host state
  intact. Normal close still terminates only the selected tmux client.
- One-active-client architecture (output slots, dispatch gating, token
  ownership, generation guards) is untouched; fresh Pi launches and
  non-Pi agents behave exactly as before.

## Notes

- tmux discovery is an injected closure (`resume_terminal_launch_with`) so
  tests can point at missing, too-old, or hung tmux binaries.
- `remove_shared_terminals` returns terminals in `terminal_order`, keeping
  the terminate result deterministic despite the HashMap storage.
- `TermSession::terminate` no longer blocks on a stubborn child:
  portable-pty's `kill` is SIGHUP-only and a blocking wait wedged on
  macOS, so close now polls `try_wait` with SIGKILL retries under a 5s
  deadline.
## Summary

- Add `shared_resume_tests` covering the focused contract: two
  tmux-client `TermSession`s get distinct `OutputSenderSlot`s; normal
  close removes only the selected client; explicit shared termination
  removes every matching terminal id in terminal order while leaving
  other shared and plain terminals untouched.
- Resume-path tests inject tmux discovery: a missing binary, a too-old
  stub (`tmux 3.2`), and a hung stub (5s probe timeout) each fail the
  resume before any `TermSession` is inserted and without a direct-Pi
  fallback; non-shared actors keep the exact direct launch; blank ids
  fail before anything spawns.

## Notes

- Plan-sanctioned split from `feat(host): resume Pi sessions through
  tmux` to respect the 400-line per-commit budget; both commits build
  independently.
- Test terminals spawn a self-exiting PTY child so reaping is instant
  and hermetic; real-tmux lifecycle coverage stays in the ignored
  private-socket suite (todo deltaqdev#9).
## Summary
- Add runtime-only shared-session state to WorkspaceState (serde-skipped,
  per-slug snapshot with replace/remove/clear that notify and clear on
  disconnect) fed by AgentShareList polls.
- Poll AgentShareList("pi") from a cancellable view-owned task every two
  seconds while the global sessions view or the Pi detail view is open;
  requests never overlap; unavailable tmux logs one deduped warning and
  leaves persisted history fully usable.
- Merge persisted rows with the live tmux snapshot in a pure helper by
  (slug, session_id): history keeps its order and timestamps, shares
  without a persisted row are never fabricated, dead panes stay attached.

## Notes
- Badge rendering and tap behavior land in the next commit; terminate in
  the one after.
- AgentSessions now receives the WorkspaceState entity like AgentDetail.
## Summary
- Merge tests: live/past attachment through day grouping (a live share
  never reorders persisted history), dead panes stay attached with exit
  codes, shares without a persisted row are never fabricated, duplicate
  snapshot entries collapse to the first, foreign slugs never pick up
  Pi shares.
- WorkspaceState tests: poll snapshots replace with change reporting,
  identical snapshots are no-ops, unavailability warns once per stretch
  and resets on recovery, share removal matches exactly one session id,
  and disconnect clearing drops the whole snapshot.

## Notes
- Plan-sanctioned test split from the previous commit to stay within the
  400-line budget.
## Summary

- Extend SessionCardProps with an optional shared snapshot and render a
  small semantic status badge on session cards in both the global
  sessions view and the Pi detail view: green Live for a running pane,
  muted Ended (or Ended (code N) when the host reports an exit code)
  for a dead pane.
- Add a pure SharedSessionStatus mapping (live/dead/exit code to label
  and semantic color selection) used by the badge renderer.
- Surface the host's actionable resume failure reason (missing or old
  tmux, prepare failure) in the existing resume failure alert instead
  of a generic message.

## Notes

- Tap behavior is unchanged: cards still dispatch ResumeAgentSession;
  the host resume helper decides tmux attach versus direct launch, so
  an older host keeps its prior direct-resume behavior.
- No raw tmux names, targets, commands, or socket paths are rendered.
## Summary

- Add workspace_action::TerminateSharedAgentSession { slug, session_id }
  and offer it from session cards via long-press only when the row
  carries a shared tmux snapshot, so plain past rows and foreign tmux
  sessions never offer termination.
- Trigger the standard medium haptic and a native destructive
  confirmation (Terminate shared session? / This stops Pi and
  disconnects every attached terminal. / Terminate / Cancel) and route
  the confirmed action through Workspace's pending-native-action slot.
- Handle the action by calling AgentShareTerminate through the session
  handle: a downgraded old host fails fast with the standard
  unavailable error instead of hanging. On success, remove the returned
  terminal IDs through the same local terminal-card cleanup path used
  by normal close and drop the shared snapshot so the row immediately
  falls back to persisted history. On failure, keep every card and
  state entry and show the host's exact error, mirroring the resume
  alert pattern.
- Test the terminate-offer predicate (shared snapshot present vs
  absent) as a pure helper next to the card renderer.

## Notes

- Closing a terminal card remains client-only: explicit termination is
  the only path that stops the shared Pi process and disconnects every
  attached terminal.
## Summary

- Add the ignored `tmux_shared_session_lifecycle` integration test: real tmux
  on a per-run private `-L` socket with drop-guard `kill-server` cleanup. It
  races eight concurrent `new-session -d -A` prepares (exactly one inner
  process), attaches two pty clients through the production attach command,
  proves simultaneous input from both, title/cwd metadata tracking, detach
  without killing Pi, reattach, and explicit termination that ends Pi and
  every attached client. Skips cleanly without tmux or below 3.3a via
  `supported_version`.
- Fix `prepare_session` retrying the already-prepared answer (rc 1 + "open
  terminal failed: not a terminal"), which failed every resume of a live
  shared session; the retry stays reserved for fresh-socket connect races.
  Stub-binary regression test added.

## Notes

- Client names differ across tmux releases (tty path on 3.7, client-<pid> on
  3.3a), so the test selects clients by `client_pid` and detaches by that
  record's own name.
- Focused host coverage for distinct output slots, selective close/terminate,
  and missing/old/hung tmux already exists in `rpc_daemon::shared_resume_tests`;
  sequential reconnect after a first client disconnect is already proven by
  `test_second_client_auth_succeeds_after_first_client_close`, so no new test
  was added for it.
## Summary

- MANAGED_AGENTS.md: describe Pi shared sessions — the `zedra-pi-<hex>`
  ownership codec, actor command authority, tmux 3.3a capability and
  actionable errors with no direct-resume fallback, close-versus-terminate
  semantics, the fresh-launch limitation, and the lossy-metadata/scrollback
  limitations.
- ARCHITECTURE.md: document the shared-session process topology — tmux owns
  the Pi PTY, macOS SSH and each Zedra terminal attach as independent tmux
  clients, `zedra-host` restart preserves tmux while container recreation
  does not, and one-active-Zedra-client is intentionally preserved.
- MANUAL_TEST.md: add section 29 release gate — simultaneous macOS SSH plus
  one Zedra client with two cards, independent maximize resize rule
  (120x40 + 38x100 -> 120x99), copy-mode scrollback, close/detach versus
  terminate, tmux-server-loss fallback to past-only state, host restart,
  `Host occupied` for a simultaneous second Zedra device, sequential
  handoff, and behavior without tmux.

## Notes

- Zedra local scrollback starts at attachment time; older history lives in
  tmux copy mode and is not replayed into Alacritty.
- The proven resize rule maximizes width and height independently across
  interactive clients under `window-size largest`; the docs avoid
  overclaiming a single "largest client" dimension.
## Summary
- With `exit-empty on` (tmux default), killing the last owned session
  exits the server; a subsequent terminate on the same or another row
  then fails with `no server running on ...` even though nothing is
  left to terminate.
- Extend the terminate success condition with the existing
  `is_no_server` check, matching `list_session_names`'s mapping of
  both proven no-server stderr variants, so `terminate_session`
  accepts `can't find session:` OR a dead server as already
  terminated.

## Notes
- Regression test `terminate_session_accepts_no_server_as_terminated`
  mirrors `prepare_session_accepts_already_prepared_session` via a
  stub `tmux` binary that fails `kill-session` with
  `no server running on /tmp/tmux-0/stub`.
Generalize shared sessions across registered actors and add the detected non-owned tmux lifecycle.
@bu5hm4nn
bu5hm4nn force-pushed the feat/tmux-agent-sessions branch from 980d870 to 369e367 Compare September 10, 2026 16:30
bu5hm4nn and others added 2 commits September 10, 2026 18:36
The branch added ios_cancel_keypad_composer and removed
zedra_ios_extended_keypad in the device header, Rust bridge, and
NativeBridge.swift, but the simulator slice of the xcframework header
was left stale. Align it so both slices declare the same FFI surface.
Co-authored-by: Codex <codex@openai.com>
@bu5hm4nn bu5hm4nn changed the title Feat/tmux agent sessions feat: support agent-backed tmux sessions Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

< 3732 /include-fragment>

1 participant

0