Skip to content

Give each secondmate its own tmux session instead of sharing the main firstmate's session #576

Description

@boyangwang

Problem

On the default tmux backend, a --secondmate spawn does not get its own tmux session. In bin/fm-spawn.sh the tmux branch resolves the session the same way for every kind:

W="fm-$ID"
case "$BACKEND" in
  tmux)
    SES=$(fm_backend_tmux_container_ensure)
    T="$SES:$W"

fm_backend_tmux_container_ensure (in bin/backends/tmux.sh) reuses the current tmux session when firstmate is already running inside one, and only falls back to creating a session literally named firstmate when it is not:

fm_backend_tmux_container_ensure() {
  if [ -n "${TMUX:-}" ]; then
    tmux display-message -p '#S'
  else
    tmux has-session -t firstmate 2>/dev/null || tmux new-session -d -s firstmate
    printf 'firstmate'
  fi
}

Because a secondmate is launched by the main firstmate from inside the firstmate session, container_ensure returns that same session. So the secondmate agent (kind=secondmate) is created as just another window (fm-<id>) inside the main firstmate's session. Worse, the secondmate's own crewmates then inherit that same session through the default current-session path.

The net result is that a single tmux session ends up mixing four different things:

  • the main firstmate
  • the main firstmate's crewmates
  • the secondmate (which is itself a firstmate)
  • the secondmate's crewmates

A secondmate is architecturally a firstmate in its own isolated home, so having its runtime windows interleaved into the parent's session is a leaky abstraction. It makes the session hard to read, hard to attach to selectively, and blurs the ownership boundary the secondmate design otherwise keeps clean.

Proposed fix

For KIND=secondmate on the tmux backend, give the secondmate its own tmux session named by its id, creating it if it does not exist. The secondmate's own crewmates then inherit that session through the normal current-session path, so each layer of the fleet stays partitioned by session:

  • session firstmate (or the operator's booted firstmate session): main firstmate + its crewmates
  • session <secondmate-id>: that secondmate + its crewmates

The name is derived from the id, so it is stable and durable across every respawn (recovery, restart, config-driven relaunch) with no extra per-task state.

Patch

--- a/bin/fm-spawn.sh
+++ b/bin/fm-spawn.sh
@@ -695,7 +695,17 @@ validate_spawn_worktree() {  # <source> <inspect-target>
 W="fm-$ID"
 case "$BACKEND" in
   tmux)
-    SES=$(fm_backend_tmux_container_ensure)
+    if [ "$KIND" = secondmate ]; then
+      # A secondmate gets its OWN tmux session named by its id, isolated from the
+      # main firstmate's session (which container_ensure would otherwise return).
+      # Its own crewmates then inherit that session via the default current-session
+      # path, so a single session never mixes firstmate + its crewmates + a
+      # secondmate + the secondmate's crewmates. Durable across every respawn.
+      SES=$ID
+      tmux has-session -t "$SES" 2>/dev/null || tmux new-session -d -s "$SES"
+    else
+      SES=$(fm_backend_tmux_container_ensure)
+    fi
     T="$SES:$W"

Notes and scope

  • The change is scoped to the tmux backend (the verified default). The herdr, zellij, orca, and cmux backends already scope workspaces differently; whether they need an analogous per-secondmate isolation is worth a follow-up, but is out of scope here.
  • No change to recovery/reconciliation is required: state/<id>.meta records window=<session>:fm-<id>, so the recorded backend target already round-trips through the new session name.
  • This pairs naturally with keeping the main firstmate always in a session named firstmate (the fallback name container_ensure already assumes), which keeps the top-level session name stable rather than timestamped.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions