From 7342415a326bbc6a9f9d57755a2e13e22e90eb9e Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Mon, 22 Jun 2026 19:18:01 +0000 Subject: [PATCH] fix(fleet): consume model_hint + fix socket-default trap (stand-up fixes) (#627) Co-authored-by: Jason Woltje Co-committed-by: Jason Woltje --- docs/TASKS.md | 4 + docs/scratchpads/fleet-standup-fixes.md | 28 +++++ .../systemd/user/mosaic-agent@.service | 6 +- .../tools/fleet/start-agent-session.sh | 28 +++-- packages/mosaic/src/commands/fleet.spec.ts | 86 +++++++++++---- packages/mosaic/src/commands/fleet.ts | 102 ++++++++++-------- 6 files changed, 180 insertions(+), 74 deletions(-) create mode 100644 docs/scratchpads/fleet-standup-fixes.md diff --git a/docs/TASKS.md b/docs/TASKS.md index 0950127..7309af8 100644 --- a/docs/TASKS.md +++ b/docs/TASKS.md @@ -74,3 +74,7 @@ Active workstream is **W1 — Federation v1**. Workers should: ## Fleet onboarding-injection — comms cheat-sheet + peer roster (#620) — feat/fleet-comms-onboarding - Status: implemented + tested. Injects # Fleet Comms (peer roster + cross-host agent-send commands + FLIP-reply + --verify) into each spawned fleet agent via composeContract; optional per-agent host/ssh/socket roster fields (socket: named → -L, unset → default socket no -L). 10 + 2 tests green. Detail: scratchpads/fleet-comms-onboarding.md. + +## Fleet stand-up fixes — model_hint→--model + socket-default trap (#626) — feat/fleet-standup-fixes + +- Status: implemented + tested. FIX1 model_hint→MOSAIC_AGENT_MODEL→--model. FIX2 absent socket = default tmux socket (no -L) across parse/spawn/systemd-unit/observe (socketArgs helper, bare-empty shellEnvValue, conditional -L). 158 fleet tests green; shipped presets unaffected (explicit socket_name). Detail: scratchpads/fleet-standup-fixes.md. diff --git a/docs/scratchpads/fleet-standup-fixes.md b/docs/scratchpads/fleet-standup-fixes.md new file mode 100644 index 0000000..0035def --- /dev/null +++ b/docs/scratchpads/fleet-standup-fixes.md @@ -0,0 +1,28 @@ +# Fleet stand-up fixes — model_hint→--model + socket-default trap (#626) + +- **Issue:** #626 · **Branch:** `feat/fleet-standup-fixes` (off main). PoC-blocking, before doctrine doc. + +## FIX 1 — model_hint consumed + +- generateAgentEnv emits `MOSAIC_AGENT_MODEL=` (bare empty when unset). +- start-agent-session.sh default command → `mosaic yolo $RUNTIME ${MOSAIC_AGENT_MODEL:+--model $MOSAIC_AGENT_MODEL}`. + → pi workers launch with `--model openai-codex/gpt-5.5:high`. + +## FIX 2 — socket default trap (absent ⇒ literal default socket, no -L everywhere) + +- THE TRAP (3 sites): parseRosterText fallback was DEFAULT_SOCKET_NAME; systemd unit had + `Environment=MOSAIC_TMUX_SOCKET=mosaic-factory` + `ExecStop ${…:-mosaic-factory}`; start-agent-session + defaulted `:-mosaic-factory`. All fixed → absent socket = '' = default tmux socket (no -L). +- `socketArgs(name)` helper → `name ? ['-L', name] : []`; replaced all ~15 -L render sites in fleet.ts. +- shellEnvValue('') now emits a **bare** `VAR=` (not `''`) — unambiguous empty in systemd EnvironmentFile + (a quoted '' could become a literal socket named "''"). +- start-agent-session.sh: `_tmux` wrapper passes -L only when socket set; mosaic-agent@.service: dropped the + socket default + conditional ExecStop. So spawn == observe == onboarding cheat-sheet. +- CONTAINMENT: all 6 shipped presets set socket_name: mosaic-factory explicitly → unaffected; only + socket-less rosters (the PoC) get default-socket behavior. DEFAULT_SOCKET_NAME exported for explicit use. + +## Verification + +- 158 fleet + 201 fleet-adjacent tests green; new: socketArgs none/named, model_hint→env, explicit-socket + renders -L, socket-less env bare. tsc/eslint/prettier/sanitize clean. Shell bash -n + end-to-end sim + (socket-less→no -L, model→--model). diff --git a/packages/mosaic/framework/systemd/user/mosaic-agent@.service b/packages/mosaic/framework/systemd/user/mosaic-agent@.service index db6491a..0ebdec9 100644 --- a/packages/mosaic/framework/systemd/user/mosaic-agent@.service +++ b/packages/mosaic/framework/systemd/user/mosaic-agent@.service @@ -8,13 +8,15 @@ PartOf=mosaic-tmux-holder.service [Service] Type=oneshot RemainAfterExit=yes -Environment=MOSAIC_TMUX_SOCKET=mosaic-factory +# No default MOSAIC_TMUX_SOCKET: an absent roster socket means the literal +# default tmux socket (no -L). The per-agent .env sets it when the roster names +# one; otherwise it stays unset and start-agent-session.sh uses the default socket. Environment=MOSAIC_AGENT_NAME=%i Environment=MOSAIC_AGENT_RUNTIME=pi Environment=MOSAIC_AGENT_WORKDIR=%h EnvironmentFile=-%h/.config/mosaic/fleet/agents/%i.env ExecStart=/bin/bash %h/.config/mosaic/tools/fleet/start-agent-session.sh %i -ExecStop=-/bin/bash -lc 'tmux -L "${MOSAIC_TMUX_SOCKET:-mosaic-factory}" kill-session -t "=%i"' +ExecStop=-/bin/bash -lc 'if [ -n "${MOSAIC_TMUX_SOCKET:-}" ]; then tmux -L "$MOSAIC_TMUX_SOCKET" kill-session -t "=%i"; else tmux kill-session -t "=%i"; fi' [Install] WantedBy=default.target diff --git a/packages/mosaic/framework/tools/fleet/start-agent-session.sh b/packages/mosaic/framework/tools/fleet/start-agent-session.sh index 7aacb67..59bbefa 100755 --- a/packages/mosaic/framework/tools/fleet/start-agent-session.sh +++ b/packages/mosaic/framework/tools/fleet/start-agent-session.sh @@ -2,8 +2,12 @@ set -euo pipefail AGENT_NAME=${1:-${MOSAIC_AGENT_NAME:-}} -MOSAIC_TMUX_SOCKET=${MOSAIC_TMUX_SOCKET:-mosaic-factory} +# Absent socket ⇒ the LITERAL default tmux socket (no -L). The roster's +# socket_name is honored when set; absent never silently becomes mosaic-factory +# (spawn stays consistent with the onboarding cheat-sheet + fleet ps observe). +MOSAIC_TMUX_SOCKET=${MOSAIC_TMUX_SOCKET:-} MOSAIC_AGENT_RUNTIME=${MOSAIC_AGENT_RUNTIME:-pi} +MOSAIC_AGENT_MODEL=${MOSAIC_AGENT_MODEL:-} MOSAIC_AGENT_WORKDIR=${MOSAIC_AGENT_WORKDIR:-$HOME} MOSAIC_AGENT_COMMAND=${MOSAIC_AGENT_COMMAND:-} MOSAIC_HEARTBEAT_RUN_DIR=${MOSAIC_HEARTBEAT_RUN_DIR:-${MOSAIC_HOME:-$HOME/.config/mosaic}/fleet/run} @@ -19,13 +23,25 @@ if ! command -v tmux >/dev/null 2>&1; then exit 69 fi -if tmux -L "$MOSAIC_TMUX_SOCKET" has-session -t "=${AGENT_NAME}:0.0" 2>/dev/null; then - echo "Mosaic agent session already running: $AGENT_NAME on socket $MOSAIC_TMUX_SOCKET" +# tmux wrapper: pass -L only when a socket is configured. An absent/empty socket +# means the default tmux socket (no -L), keeping spawn == observe == cheat-sheet. +_tmux() { + if [ -n "$MOSAIC_TMUX_SOCKET" ]; then + tmux -L "$MOSAIC_TMUX_SOCKET" "$@" + else + tmux "$@" + fi +} + +if _tmux has-session -t "=${AGENT_NAME}:0.0" 2>/dev/null; then + echo "Mosaic agent session already running: $AGENT_NAME on socket ${MOSAIC_TMUX_SOCKET:-(default)}" exit 0 fi if [ -z "$MOSAIC_AGENT_COMMAND" ]; then - MOSAIC_AGENT_COMMAND="mosaic yolo $MOSAIC_AGENT_RUNTIME" + # Map the roster's per-agent model_hint to `--model` so workers launch on the + # configured model (e.g. pi on openai-codex/gpt-5.5:high). Omitted when unset. + MOSAIC_AGENT_COMMAND="mosaic yolo $MOSAIC_AGENT_RUNTIME${MOSAIC_AGENT_MODEL:+ --model $MOSAIC_AGENT_MODEL}" fi # ── Derive a runtime-bin PATH prefix ───────────────────────────────────────── @@ -107,13 +123,13 @@ fi mkdir -p "$MOSAIC_AGENT_WORKDIR" # ── Launch the tmux session (no exec — we continue to wire the heartbeat) ──── -tmux -L "$MOSAIC_TMUX_SOCKET" new-session -d -s "$AGENT_NAME" -c "$MOSAIC_AGENT_WORKDIR" \ +_tmux new-session -d -s "$AGENT_NAME" -c "$MOSAIC_AGENT_WORKDIR" \ bash -c "$PANE_SHELL_SNIPPET" # ── Resolve the pane PID (retry briefly to let the session initialise) ──────── PANE_PID="" for _retry in 1 2 3 4 5; do - PANE_PID=$(tmux -L "$MOSAIC_TMUX_SOCKET" list-panes \ + PANE_PID=$(_tmux list-panes \ -t "=${AGENT_NAME}:0.0" -F '#{pane_pid}' 2>/dev/null || true) [ -n "$PANE_PID" ] && break sleep 0.2 diff --git a/packages/mosaic/src/commands/fleet.spec.ts b/packages/mosaic/src/commands/fleet.spec.ts index 66aac40..61f43f2 100644 --- a/packages/mosaic/src/commands/fleet.spec.ts +++ b/packages/mosaic/src/commands/fleet.spec.ts @@ -15,6 +15,7 @@ import { buildFleetServiceCommand, buildSystemdEnableCommand, buildSystemdDisableCommand, + socketArgs, buildSystemdShowCommand, buildTmuxListPanesCommand, buildTmuxListSessionsCommand, @@ -114,7 +115,7 @@ describe('fleet roster parsing', () => { } }); - it('defaults local canary rosters to the isolated mosaic-factory socket', async () => { + it('defaults a socket-less roster to the literal default tmux socket (empty, no -L)', async () => { cleanup = await tempDir(); const rosterPath = join(cleanup, 'roster.yaml'); await writeFile( @@ -131,12 +132,55 @@ describe('fleet roster parsing', () => { const roster = await loadFleetRoster(rosterPath); - expect(roster.tmux.socketName).toBe('mosaic-factory'); + expect(roster.tmux.socketName).toBe(''); // absent ⇒ default socket (no -L), not mosaic-factory expect(roster.tmux.holderSession).toBe('_holder'); expect(roster.agents).toHaveLength(1); expect(getRosterAgent(roster, 'canary-pi').runtime).toBe('pi'); }); + it('socketArgs: named socket → -L ; empty → no -L (default socket)', () => { + expect(socketArgs('mosaic-factory')).toEqual(['-L', 'mosaic-factory']); + expect(socketArgs('')).toEqual([]); + }); + + it('honors an explicit socket_name (renders -L) — containment for shipped presets', async () => { + cleanup = await tempDir(); + const rosterPath = join(cleanup, 'roster.yaml'); + await writeFile( + rosterPath, + [ + 'version: 1', + 'transport: tmux', + 'tmux:', + ' socket_name: mosaic-factory', + 'agents:', + ' - name: canary-pi', + ' runtime: pi', + ].join('\n'), + ); + const roster = await loadFleetRoster(rosterPath); + expect(roster.tmux.socketName).toBe('mosaic-factory'); + expect(buildTmuxListSessionsCommand(roster.tmux.socketName)).toContain('-L'); + }); + + it('maps a per-agent model_hint into MOSAIC_AGENT_MODEL', async () => { + cleanup = await tempDir(); + const rosterPath = join(cleanup, 'roster.json'); + await writeFile( + rosterPath, + JSON.stringify({ + version: 1, + transport: 'tmux', + agents: [{ name: 'coder0', runtime: 'pi', model_hint: 'openai-codex/gpt-5.5:high' }], + }), + ); + const roster = await loadFleetRoster(rosterPath); + const env = generateAgentEnv(roster, getRosterAgent(roster, 'coder0')); + expect(env).toContain('MOSAIC_AGENT_MODEL=openai-codex/gpt-5.5:high'); + // socket-less roster ⇒ a bare empty socket (no quotes), so spawn uses no -L + expect(env).toContain('MOSAIC_TMUX_SOCKET=\n'); + }); + it('generates deterministic per-agent EnvironmentFile content', async () => { cleanup = await tempDir(); const rosterPath = join(cleanup, 'roster.json'); @@ -156,6 +200,7 @@ describe('fleet roster parsing', () => { [ 'MOSAIC_AGENT_NAME=coder0', 'MOSAIC_AGENT_RUNTIME=codex', + 'MOSAIC_AGENT_MODEL=', 'MOSAIC_AGENT_WORKDIR=/srv/mosaic', 'MOSAIC_TMUX_SOCKET=mosaic-factory', '', @@ -355,8 +400,9 @@ describe('fleet command construction', () => { try { await program.parseAsync(['node', 'mosaic', 'fleet', 'verify']); expect(calls).toEqual([ - ['tmux', '-L', 'mosaic-factory', 'has-session', '-t', '=_holder:0.0'], - ['tmux', '-L', 'mosaic-factory', 'has-session', '-t', '=coder0:0.0'], + // socket-less roster ⇒ default tmux socket (no -L) + ['tmux', 'has-session', '-t', '=_holder:0.0'], + ['tmux', 'has-session', '-t', '=coder0:0.0'], ]); } finally { await rm(home, { recursive: true, force: true }); @@ -637,7 +683,7 @@ describe('fleet command construction', () => { try { await program.parseAsync(['node', 'mosaic', 'agent', 'status', 'json-agent']); expect(calls).toEqual([ - ['tmux', '-L', 'mosaic-factory', 'has-session', '-t', '=json-agent:0.0'], + ['tmux', 'has-session', '-t', '=json-agent:0.0'], // socket-less ⇒ no -L ]); } finally { await rm(home, { recursive: true, force: true }); @@ -677,8 +723,6 @@ describe('fleet command construction', () => { expect(calls).toEqual([ [ join(home, 'tools', 'tmux', 'agent-send.sh'), - '-L', - 'mosaic-factory', '-S', getDefaultOperatorSourceLabel(), '-s', @@ -727,8 +771,6 @@ describe('fleet command construction', () => { expect(calls).toEqual([ [ join(home, 'tools', 'tmux', 'agent-send.sh'), - '-L', - 'mosaic-factory', '-S', 'lead:manual', '-s', @@ -811,9 +853,11 @@ describe('fleet ps — command construction', () => { ]); }); - it('uses DEFAULT_SOCKET_NAME when socket is omitted from list-panes', () => { + it('uses the default tmux socket (no -L) when socket is omitted from list-panes', () => { const cmd = buildTmuxListPanesCommand('canary-pi'); - expect(cmd[2]).toBe('mosaic-factory'); + expect(cmd).not.toContain('-L'); // omitted socket ⇒ default socket + expect(cmd[0]).toBe('tmux'); + expect(cmd[1]).toBe('list-panes'); }); it('derives heartbeat path under ~/.config/mosaic/fleet/run/', () => { @@ -1331,8 +1375,9 @@ describe('fleet ps — command sequences issued', () => { await program.parseAsync(['node', 'mosaic', 'fleet', 'ps']); expect(calls).toEqual([ buildSystemdShowCommand('coder0'), - buildTmuxListPanesCommand('coder0', 'mosaic-factory'), - buildTmuxListSessionsCommand('mosaic-factory'), + // socket-less roster ⇒ default socket (no -L) + buildTmuxListPanesCommand('coder0'), + buildTmuxListSessionsCommand(), ]); } finally { console.log = origLog; @@ -1353,9 +1398,10 @@ describe('buildTmuxListSessionsCommand', () => { ]); }); - it('uses DEFAULT_SOCKET_NAME when socket is omitted', () => { + it('uses the default tmux socket (no -L) when socket is omitted', () => { const cmd = buildTmuxListSessionsCommand(); - expect(cmd[2]).toBe('mosaic-factory'); + expect(cmd).not.toContain('-L'); + expect(cmd).toEqual(['tmux', 'list-sessions', '-F', '#{session_name}']); }); }); @@ -1633,9 +1679,10 @@ describe('agent watch', () => { ]); }); - it('buildAgentWatchCommand (deprecated) still uses DEFAULT_SOCKET_NAME when socket is omitted', () => { + it('buildAgentWatchCommand (deprecated) uses the default tmux socket (no -L) when socket is omitted', () => { const cmd = buildAgentWatchCommand('canary-pi'); - expect(cmd[2]).toBe('mosaic-factory'); + expect(cmd).not.toContain('-L'); // omitted socket ⇒ default socket + expect(cmd[0]).toBe('tmux'); expect(cmd).toContain('-r'); }); @@ -1829,9 +1876,10 @@ describe('agent send --verify', () => { // 3 calls: BEFORE-capture, send, AFTER-capture (pane changed on first poll → accepted immediately) expect(calls).toHaveLength(3); - expect(calls[0]).toEqual(buildAgentVerifyAcceptedCommand('coder0', 'mosaic-factory', 5)); + // socket-less roster ⇒ default socket (no -L) + expect(calls[0]).toEqual(buildAgentVerifyAcceptedCommand('coder0', '', 5)); expect(calls[1]![0]).toContain('agent-send.sh'); - expect(calls[2]).toEqual(buildAgentVerifyAcceptedCommand('coder0', 'mosaic-factory', 5)); + expect(calls[2]).toEqual(buildAgentVerifyAcceptedCommand('coder0', '', 5)); } finally { await rm(home, { recursive: true, force: true }); } diff --git a/packages/mosaic/src/commands/fleet.ts b/packages/mosaic/src/commands/fleet.ts index 20ae4b3..cca0467 100644 --- a/packages/mosaic/src/commands/fleet.ts +++ b/packages/mosaic/src/commands/fleet.ts @@ -117,10 +117,26 @@ export interface FleetPaths { type FleetServiceAction = 'start' | 'stop' | 'restart' | 'status'; -const DEFAULT_SOCKET_NAME = 'mosaic-factory'; +/** + * The named tmux socket the canonical fleet uses. Kept as a public constant for + * rosters/callers that explicitly want isolation; it is NO LONGER the silent + * fallback for a socket-less roster (that now resolves to the default socket). + */ +export const DEFAULT_SOCKET_NAME = 'mosaic-factory'; const DEFAULT_HOLDER_SESSION = '_holder'; const DEFAULT_WORKING_DIRECTORY = '~/src'; +/** + * tmux `-L` args for a socket name. An empty/absent socket ⇒ the LITERAL default + * tmux socket (no `-L`), so spawn, observe (`fleet ps`/watch), and the onboarding + * cheat-sheet all agree. A named socket ⇒ `-L `. `DEFAULT_SOCKET_NAME` + * remains a constant for callers that explicitly want mosaic-factory; it is no + * longer the silent fallback for a socket-less roster. + */ +export function socketArgs(socketName: string): string[] { + return socketName ? ['-L', socketName] : []; +} + /** * Default poll interval (ms) between capture-pane checks in `send --verify`. * Kept short enough to react quickly while not hammering tmux on busy hosts. @@ -185,6 +201,10 @@ export function generateAgentEnv(roster: FleetRoster, agent: FleetAgent): string return [ `MOSAIC_AGENT_NAME=${shellEnvValue(agent.name)}`, `MOSAIC_AGENT_RUNTIME=${shellEnvValue(agent.runtime)}`, + // Per-agent model hint → start-agent-session.sh appends `--model ` to + // the `mosaic yolo` launch so workers run on the roster's model (e.g. pi on + // openai-codex/gpt-5.5:high). Empty when the agent declares no model_hint. + `MOSAIC_AGENT_MODEL=${shellEnvValue(agent.modelHint ?? '')}`, `MOSAIC_AGENT_WORKDIR=${shellEnvValue(expandHome(workingDirectory))}`, `MOSAIC_TMUX_SOCKET=${shellEnvValue(roster.tmux.socketName)}`, '', @@ -319,13 +339,12 @@ export function buildAgentSendCommand( paths: FleetPaths, agentName: string, message: string, - socketName = DEFAULT_SOCKET_NAME, + socketName = '', sourceLabel = getDefaultOperatorSourceLabel(), ): string[] { return [ join(paths.tmuxToolsDir, 'agent-send.sh'), - '-L', - socketName, + ...socketArgs(socketName), '-S', sourceLabel, '-s', @@ -344,12 +363,11 @@ export function buildAgentResetCommand( paths: FleetPaths, agentName: string, resetCommand: string, - socketName = DEFAULT_SOCKET_NAME, + socketName = '', ): string[] { return [ join(paths.tmuxToolsDir, 'send-message.sh'), - '-L', - socketName, + ...socketArgs(socketName), '-t', `=${agentName}`, '-m', @@ -357,15 +375,10 @@ export function buildAgentResetCommand( ]; } -export function buildAgentTailCommand( - agentName: string, - lines: number, - socketName = DEFAULT_SOCKET_NAME, -): string[] { +export function buildAgentTailCommand(agentName: string, lines: number, socketName = ''): string[] { return [ 'tmux', - '-L', - socketName, + ...socketArgs(socketName), 'capture-pane', '-t', `=${agentName}:0.0`, @@ -449,14 +462,10 @@ export function buildSystemdShowCommand(agentName: string): string[] { * Returns the tmux list-panes command for an agent pane. * Format: `#{pane_pid} #{pane_current_command} #{pane_dead} #{pane_activity}` */ -export function buildTmuxListPanesCommand( - agentName: string, - socketName = DEFAULT_SOCKET_NAME, -): string[] { +export function buildTmuxListPanesCommand(agentName: string, socketName = ''): string[] { return [ 'tmux', - '-L', - socketName, + ...socketArgs(socketName), 'list-panes', '-t', `=${agentName}:0.0`, @@ -470,8 +479,8 @@ export function buildTmuxListPanesCommand( * Format: `tmux -L list-sessions -F '#{session_name}'` * Used to discover ad-hoc sessions that are not in the roster. */ -export function buildTmuxListSessionsCommand(socketName = DEFAULT_SOCKET_NAME): string[] { - return ['tmux', '-L', socketName, 'list-sessions', '-F', '#{session_name}']; +export function buildTmuxListSessionsCommand(socketName = ''): string[] { + return ['tmux', ...socketArgs(socketName), 'list-sessions', '-F', '#{session_name}']; } /** @@ -653,12 +662,11 @@ export function getDefaultTenantAndHost(): { tenant_id: string; host: string } { export function buildAgentWatchCreateViewerCommand( agentName: string, viewerSessionName: string, - socketName = DEFAULT_SOCKET_NAME, + socketName = '', ): string[] { return [ 'tmux', - '-L', - socketName, + ...socketArgs(socketName), 'new-session', '-d', '-t', @@ -672,11 +680,8 @@ export function buildAgentWatchCreateViewerCommand( * Builds the interactive attach command for a viewer session (read-only). * Must be run via interactiveRunner (stdio: 'inherit'). */ -export function buildAgentWatchAttachCommand( - viewerSessionName: string, - socketName = DEFAULT_SOCKET_NAME, -): string[] { - return ['tmux', '-L', socketName, 'attach', '-r', '-t', viewerSessionName]; +export function buildAgentWatchAttachCommand(viewerSessionName: string, socketName = ''): string[] { + return ['tmux', ...socketArgs(socketName), 'attach', '-r', '-t', viewerSessionName]; } /** @@ -685,9 +690,9 @@ export function buildAgentWatchAttachCommand( */ export function buildAgentWatchKillViewerCommand( viewerSessionName: string, - socketName = DEFAULT_SOCKET_NAME, + socketName = '', ): string[] { - return ['tmux', '-L', socketName, 'kill-session', '-t', viewerSessionName]; + return ['tmux', ...socketArgs(socketName), 'kill-session', '-t', viewerSessionName]; } /** @@ -705,11 +710,8 @@ export function buildViewerSessionName(agentName: string): string { * * Kept for backward compatibility only. */ -export function buildAgentWatchCommand( - agentName: string, - socketName = DEFAULT_SOCKET_NAME, -): string[] { - return ['tmux', '-L', socketName, 'attach', '-r', '-t', `=${agentName}`]; +export function buildAgentWatchCommand(agentName: string, socketName = ''): string[] { + return ['tmux', ...socketArgs(socketName), 'attach', '-r', '-t', `=${agentName}`]; } /** @@ -719,13 +721,12 @@ export function buildAgentWatchCommand( */ export function buildAgentVerifyAcceptedCommand( agentName: string, - socketName = DEFAULT_SOCKET_NAME, + socketName = '', lines = 5, ): string[] { return [ 'tmux', - '-L', - socketName, + ...socketArgs(socketName), 'capture-pane', '-t', `=${agentName}:0.0`, @@ -989,8 +990,7 @@ export function registerFleetCommand(program: Command, deps: FleetCommandDeps = const socketName = roster.tmux.socketName; await runChecked(runner, [ 'tmux', - '-L', - socketName, + ...socketArgs(socketName), 'has-session', '-t', `=${roster.tmux.holderSession}:0.0`, @@ -998,8 +998,7 @@ export function registerFleetCommand(program: Command, deps: FleetCommandDeps = for (const agent of roster.agents) { await runChecked(runner, [ 'tmux', - '-L', - socketName, + ...socketArgs(socketName), 'has-session', '-t', `=${agent.name}:0.0`, @@ -1370,8 +1369,8 @@ export function registerFleetAgentCommands( getRosterAgent(roster, agent); } const command = agent - ? ['tmux', '-L', roster.tmux.socketName, 'has-session', '-t', `=${agent}:0.0`] - : ['tmux', '-L', roster.tmux.socketName, 'ls']; + ? ['tmux', ...socketArgs(roster.tmux.socketName), 'has-session', '-t', `=${agent}:0.0`] + : ['tmux', ...socketArgs(roster.tmux.socketName), 'ls']; const result = await runner(...splitCommand(command)); if (opts.json) { console.log( @@ -1689,9 +1688,12 @@ function normalizeRoster(raw: RawFleetRoster): FleetRoster { version: 1, transport: 'tmux', tmux: { + // Absent socket_name ⇒ '' (the literal default tmux socket, no -L) — NOT + // mosaic-factory. Shipped presets set socket_name explicitly, so they are + // unaffected; only socket-less rosters get default-socket behavior. socketName: stringValue( raw.tmux?.socket_name ?? raw.tmux?.socketName, - DEFAULT_SOCKET_NAME, + '', 'Fleet roster tmux socket_name', ), holderSession: stringValue( @@ -1857,6 +1859,12 @@ function expandHome(path: string): string { } function shellEnvValue(value: string): string { + // Empty ⇒ a bare `VAR=` (unambiguous empty in a systemd EnvironmentFile and + // when shell-sourced). Quoting it as '' risks a literal two-char value (e.g. + // a tmux socket named "''"), which would defeat the default-socket behavior. + if (value === '') { + return ''; + } if (/^[A-Za-z0-9_./:=@+-]+$/.test(value)) { return value; }