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.
Problem
On the default
tmuxbackend, a--secondmatespawn does not get its own tmux session. Inbin/fm-spawn.shthe tmux branch resolves the session the same way for every kind:fm_backend_tmux_container_ensure(inbin/backends/tmux.sh) reuses the current tmux session when firstmate is already running inside one, and only falls back to creating a session literally namedfirstmatewhen it is not:Because a secondmate is launched by the main firstmate from inside the firstmate session,
container_ensurereturns 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:
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=secondmateon 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:firstmate(or the operator's booted firstmate session): main firstmate + its crewmates<secondmate-id>: that secondmate + its crewmatesThe 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
Notes and scope
tmuxbackend (the verified default). Theherdr,zellij,orca, andcmuxbackends already scope workspaces differently; whether they need an analogous per-secondmate isolation is worth a follow-up, but is out of scope here.state/<id>.metarecordswindow=<session>:fm-<id>, so the recorded backend target already round-trips through the new session name.firstmate(the fallback namecontainer_ensurealready assumes), which keeps the top-level session name stable rather than timestamped.