tmux

package
v1.38.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: AGPL-3.0 Imports: 32 Imported by: 0

Documentation

Index

Constants

View Source
const LegacyTmuxPrefix = "claudesquad_"
View Source
const ProgramAider = "aider"
View Source
const ProgramClaude = "claude"
View Source
const ProgramGemini = "gemini"
View Source
const TmuxPrefix = "staplersquad_"

Variables

View Source
var (
	// ErrControlModeNotRunning is returned when sendCMCommand is called but control mode is not active.
	ErrControlModeNotRunning = errors.New("control mode not running")
	// ErrControlModeStopped is sent to all in-flight commands when the control mode process exits.
	ErrControlModeStopped = errors.New("control mode stopped")
)
View Source
var ErrRegistryUnavailable = errors.New("tmux registry unavailable")

ErrRegistryUnavailable is returned when the registry is not healthy and cannot serve a request that requires the registry to be up.

View Source
var ErrServerDown = errors.New("tmux server not running")

ErrServerDown is returned by ListAllSessions when the tmux server is not running. Callers should treat this as "no sessions are alive" without attempting recovery.

Functions

func AcquireExecSlot added in v1.37.0

func AcquireExecSlot(ctx context.Context, serverSocket string) (release func(), err error)

AcquireExecSlot blocks until a tmux-subprocess execution slot is free — across this and every other process touching the same tmux server — or ctx is done. release must be called exactly once, after the subprocess this slot guards has fully exited (after Output()/Run()/CombinedOutput()/Wait() returns, not merely after Start()). The returned closure is idempotent: an accidental double-release is a safe no-op rather than a double-unlock.

tmux's server is single-threaded (confirmed from its own source: a libevent event loop with no worker threads, doing an unconditional O(n) scan of every connected client on each wakeup) and gets measurably slower as concurrent load and client count rise. This bounds how many tmux subprocesses can be in flight at once so callers queue client-side instead of piling onto the server.

func Binary added in v1.21.0

func Binary() string

Binary returns the tmux executable path. TMUX_BIN env var overrides the default "tmux" — set it to use a specific binary (e.g. TMUX_BIN=$(pwd)/bin/tmux go test or the pinned submodule build).

To bundle tmux directly into the stapler-squad binary instead, build with:

go build -tags embed_tmux .

after running: make build-tmux-embed

func CleanupSessions

func CleanupSessions(cmdExec executor.Executor) error

CleanupSessions kills all tmux sessions that start with "session-" on the default server

func CleanupSessionsOnServer

func CleanupSessionsOnServer(cmdExec executor.Executor, serverSocket string) error

CleanupSessionsOnServer kills all tmux sessions that start with "session-" on a specific server serverSocket: socket name for server isolation, empty string for default server

func CreateKeepaliveSession added in v1.1.0

func CreateKeepaliveSession(serverSocket string) error

CreateKeepaliveSession creates a hidden tmux session that keeps the server alive. The session runs an idle shell and is intentionally never cleaned up by stapler-squad. As long as this session exists, the tmux server cannot exit due to having no sessions.

func IsServerDown added in v1.15.0

func IsServerDown(serverSocket string) bool

IsServerDown returns true if the tmux server is not running for the given socket. Returns false if the server state cannot be determined (treats unknown as up to avoid false-positive zombie recovery suppression).

func ListAllSessions added in v1.15.0

func ListAllSessions(serverSocket string) (map[string]bool, error)

ListAllSessions returns the set of all currently live tmux session names. Uses serverSocket for isolation if non-empty (same -L flag semantics as TmuxSession). Does NOT go through the per-session existence cache - intended for bulk reconciliation. Returns ErrServerDown when the tmux server is not running.

func LookupChildPID added in v1.35.0

func LookupChildPID(pid int) (description string, startedAt time.Time, ok bool)

LookupChildPID returns the description and start time for a tracked PID. Returns ("unknown", zero, false) if the PID was not registered.

func NewTmuxSessionWithCleanup

func NewTmuxSessionWithCleanup(name string, program string) (*TmuxSession, CleanupFunc)

NewTmuxSessionWithCleanup creates a new TmuxSession and returns it along with a cleanup function. Usage: session, cleanup := NewTmuxSessionWithCleanup(name, program); defer cleanup()

func NewTmuxSessionWithPrefixAndCleanup

func NewTmuxSessionWithPrefixAndCleanup(name string, program string, prefix string) (*TmuxSession, CleanupFunc)

NewTmuxSessionWithPrefixAndCleanup creates a new TmuxSession with custom prefix and cleanup function. Usage: session, cleanup := NewTmuxSessionWithPrefixAndCleanup(name, program, prefix); defer cleanup()

func NewTmuxSessionWithServerSocketAndCleanup

func NewTmuxSessionWithServerSocketAndCleanup(name string, program string, prefix string, serverSocket string) (*TmuxSession, CleanupFunc)

NewTmuxSessionWithServerSocketAndCleanup creates a TmuxSession with server isolation and cleanup. Usage: session, cleanup := NewTmuxSessionWithServerSocketAndCleanup(name, program, prefix, socket); defer cleanup()

func RecordZombieProcess added in v1.24.0

func RecordZombieProcess(pid int, sessionName string, warnFn func(string, ...any))

RecordZombieProcess records detection of a zombie child process (Z state in ps). sessionName is the comm field from ps (process name). The spawn registry is checked to include the originating component in the log message.

func RegisterForkPressureAlert added in v1.24.0

func RegisterForkPressureAlert(fn AlertFunc)

RegisterForkPressureAlert registers fn to be called when fork pressure crosses a threshold. Safe to call from multiple goroutines before any subprocess spawning begins.

func RemoveServerRegistry added in v1.35.0

func RemoveServerRegistry(socket string)

RemoveServerRegistry stops and removes the TmuxServerRegistry for the given socket. This is primarily used in tests to clean up ephemeral registries.

func SetExitEmpty added in v1.1.0

func SetExitEmpty(serverSocket string, enabled bool) error

SetExitEmpty sets the tmux server-level exit-empty option. When enabled=false, the server stays alive even when all sessions are closed. Requires the server to already be running.

func SetServerRecoveryCallback added in v1.1.0

func SetServerRecoveryCallback(fn func())

SetServerRecoveryCallback registers a function called after successful server recovery. Thread-safe: the callback executes outside the recoveryMu lock, in a goroutine.

func SetSubreaper added in v1.35.0

func SetSubreaper() error

SetSubreaper makes the current process the subreaper for its entire descendant tree on Linux. When any descendant process exits and its direct parent has not yet called wait(), the kernel reparents the zombie to the nearest subreaper ancestor rather than to init (PID 1). Our existing Wait4(-1, WNOHANG) reaper then collects those zombies too, including tmux's direct children.

This is a no-op on non-Linux platforms; call it unconditionally at startup.

func StartForkPressureLogger added in v1.24.0

func StartForkPressureLogger(ctx context.Context, interval time.Duration, logFn func(string, ...any))

StartForkPressureLogger starts a background goroutine that logs fork pressure stats periodically.

func StartZombieReaper added in v1.24.0

func StartZombieReaper(ctx context.Context, interval time.Duration, logFn func(string, ...any))

StartZombieReaper starts a background goroutine that periodically reaps zombie child processes by draining Wait4(-1, WNOHANG).

This complements StartZombieWatcher: the watcher detects and alerts; the reaper actually cleans up. A zombie (Z state) by definition has no outstanding Wait4 caller—if cmd.Wait() had been called, the zombie would already be gone—so the WNOHANG wildcard wait is safe to issue without racing active Cmd goroutines.

Recommended interval: 60s (half the watcher period is plenty; slower means fewer interference opportunities with in-flight cmd.Wait calls).

func StartZombieWatcher added in v1.24.0

func StartZombieWatcher(ctx context.Context, interval time.Duration, warnFn func(string, ...any))

StartZombieWatcher starts a background goroutine that periodically scans for zombie processes and records them via RecordZombieProcess when found. ctx controls its lifetime. interval is how often to scan (recommended: 30s).

The first scan establishes a baseline: zombies already present at startup are silently added to the reported set without triggering fork-pressure alerts. Only zombies that appear after the baseline (i.e. growth over time) are recorded and counted toward the alert threshold. This prevents a burst of spurious critical alerts on service restart when a stable set of zombie children already exists.

func StopServerRegistry added in v1.20.0

func StopServerRegistry(socket string)

StopServerRegistry stops and removes the registry for the given socket. Safe to call even if no registry was ever created for the socket. After this call, GetServerRegistry(socket) will create a fresh registry. Intended for test cleanup to prevent reconnectLoop from restarting a tmux server after it has been killed.

func ToStaplerSquadTmuxName

func ToStaplerSquadTmuxName(str string) string

ToStaplerSquadTmuxName converts a string to a valid tmux session name with the default prefix

func TrackChildPID added in v1.35.0

func TrackChildPID(pid int, description string)

TrackChildPID registers a child PID with a human-readable description so zombie alerts can identify which component failed to call Wait(). Call after cmd.Start(). description should identify the component and purpose, e.g.:

"tmux control-mode session=my-session"
"tmux registry control-mode socket=/tmp/tmux.sock"

func TryAcquireExecSlot added in v1.37.0

func TryAcquireExecSlot(serverSocket string) (release func(), ok bool)

TryAcquireExecSlot is the non-blocking variant for periodic background pollers: if no slot is free right now, ok is false so the caller can skip this cycle rather than queue behind interactive traffic.

func UntrackChildPID added in v1.35.0

func UntrackChildPID(pid int)

UntrackChildPID removes a PID from the registry. Call after cmd.Wait() returns.

Types

type AlertFunc added in v1.24.0

type AlertFunc func(level ForkPressureLevel, stats ForkPressureStats)

AlertFunc is called when fork pressure crosses a threshold.

type BannerFilter

type BannerFilter struct {
	// contains filtered or unexported fields
}

BannerFilter detects and filters tmux status line banners from terminal output

func NewBannerFilter

func NewBannerFilter() *BannerFilter

NewBannerFilter creates a new banner filter with default tmux status patterns

func (*BannerFilter) FilterBanners

func (bf *BannerFilter) FilterBanners(lines []string) ([]string, int)

FilterBanners removes tmux status banners from a slice of lines Returns the filtered lines and a count of how many banners were removed

func (*BannerFilter) FilterBannersFromText

func (bf *BannerFilter) FilterBannersFromText(text string) (string, int)

FilterBannersFromText takes a multi-line string and removes banner lines Returns the filtered text and a count of banners removed

func (*BannerFilter) HasMeaningfulContent

func (bf *BannerFilter) HasMeaningfulContent(text string) bool

HasMeaningfulContent returns true if the text has content beyond just banners The last line may be a tmux status bar, but only exclude it if it matches status bar patterns

func (*BannerFilter) IsBanner

func (bf *BannerFilter) IsBanner(line string) bool

IsBanner returns true if the given line appears to be a tmux status banner

type CleanupFunc

type CleanupFunc func() error

CleanupFunc represents a cleanup function that should be deferred

type ForkPressureLevel added in v1.24.0

type ForkPressureLevel int

ForkPressureLevel describes the current subprocess pressure state.

const (
	ForkPressureOK       ForkPressureLevel = iota
	ForkPressureWarning                    // spawn rate elevated
	ForkPressureCritical                   // spawn failures detected
)

func (ForkPressureLevel) String added in v1.24.0

func (l ForkPressureLevel) String() string

type ForkPressureStats added in v1.24.0

type ForkPressureStats struct {
	TotalSpawns      int64
	TotalFailures    int64
	TotalZombies     int64
	SpawnsInWindow   int64
	FailuresInWindow int64
	ZombiesInWindow  int64
	WindowDuration   time.Duration
	Level            ForkPressureLevel
	LastAlertAt      time.Time
}

ForkPressureStats is a point-in-time snapshot of fork pressure metrics.

func ForkPressureSnapshot added in v1.24.0

func ForkPressureSnapshot() ForkPressureStats

ForkPressureSnapshot returns a point-in-time snapshot of fork pressure metrics.

type MockCmdExec

type MockCmdExec struct {
	RunFunc            func(cmd *exec.Cmd) error
	OutputFunc         func(cmd *exec.Cmd) ([]byte, error)
	CombinedOutputFunc func(cmd *exec.Cmd) ([]byte, error)
}

MockCmdExec provides mock functionality for executor.Executor interface

func (MockCmdExec) CombinedOutput

func (m MockCmdExec) CombinedOutput(cmd *exec.Cmd) ([]byte, error)

func (MockCmdExec) Output

func (m MockCmdExec) Output(cmd *exec.Cmd) ([]byte, error)

func (MockCmdExec) Run

func (m MockCmdExec) Run(cmd *exec.Cmd) error

type PaneExitSubscriber added in v1.18.0

type PaneExitSubscriber interface {
	SubscribePaneExit(ctx context.Context, sessionName string) <-chan struct{}
}

PaneExitSubscriber delivers a channel that is closed when the named pane exits. Caller selects on the returned channel alongside ctx.Done(). Cancelling ctx unregisters the subscription; channel is closed immediately.

type Pty

type Pty struct{}

Pty starts a "real" pseudo-terminal (PTY) using the creack/pty package.

func (Pty) Close

func (pt Pty) Close()

func (Pty) Start

func (pt Pty) Start(cmd *exec.Cmd) (*os.File, *exec.Cmd, error)

func (Pty) StartWithSize added in v1.35.0

func (pt Pty) StartWithSize(cmd *exec.Cmd, ws *pty.Winsize) (*os.File, *exec.Cmd, error)

type PtyFactory

type PtyFactory interface {
	Start(cmd *exec.Cmd) (*os.File, *exec.Cmd, error)
	// StartWithSize starts cmd in a new PTY with the given terminal dimensions set before the
	// child process is forked. This prevents tmux from seeing a 0×0 terminal (which causes it
	// to immediately disconnect) when the parent process has no controlling terminal.
	StartWithSize(cmd *exec.Cmd, ws *pty.Winsize) (*os.File, *exec.Cmd, error)
	Close()
}

func MakePtyFactory

func MakePtyFactory() PtyFactory

type SessionExistenceChecker added in v1.18.0

type SessionExistenceChecker interface {
	SessionExists(name string) bool
	IsHealthy() bool
}

SessionExistenceChecker answers "is session X alive right now?" Used by TmuxSession.DoesSessionExist to avoid exec.Command forks.

type SessionLister added in v1.18.0

type SessionLister interface {
	ListSessions() map[string]bool
	IsHealthy() bool
}

SessionLister returns a snapshot of all live session names. Used by PTYDiscovery and reconciliation loops.

type SessionName added in v1.37.0

type SessionName struct {
	// contains filtered or unexported fields
}

SessionName is a sanitized tmux session identifier — the only string form that is safe to pass to tmux's "-t" flag. The field is unexported so the only way to obtain one outside this package is NewSessionName: holding a SessionName proves the raw title has already been through sanitization, so callers never need to re-derive or re-sanitize it themselves.

This exists because tmux session names were historically re-derived ad hoc at each call site (creation, streaming, approval matching) using slightly different logic, which silently drifted out of sync whenever a title contained whitespace (see #162: a session was created as "staplersquad_CareerGrowth" but addressed as "staplersquad_Career Growth", making it permanently uncontrollable).

func NewSessionName added in v1.37.0

func NewSessionName(title, prefix string) SessionName

NewSessionName sanitizes a raw instance title into the tmux session name that was (or will be) used to create the session. Always call this instead of concatenating prefix+title — it is the single source of truth for the sanitization rules.

func (SessionName) String added in v1.37.0

func (n SessionName) String() string

type ShellTmuxHandle added in v1.35.0

type ShellTmuxHandle struct {
	// contains filtered or unexported fields
}

ShellTmuxHandle manages a single shell as an independent sibling tmux session.

Architecture (per adversarial review Challenge 1): Each shell is its own tmux session named "{parentName}_shell_{shellUUID}". This is NOT a window inside the parent session. Using a sibling session means:

  • attach-session -t {shellSessionName} gives a fully isolated PTY
  • killing the parent session does not affect shell sessions
  • PTY output cannot bleed between Claude terminal and shell terminals

func NewShellTmuxHandle added in v1.35.0

func NewShellTmuxHandle(sessionName, serverSocket string, ptyFactory PtyFactory, cmdExec executor.Executor) *ShellTmuxHandle

NewShellTmuxHandle creates a ShellTmuxHandle for the given sibling session name. sessionName should be the full computed name: "{parentPrefix}_shell_{shellUUID}".

func (*ShellTmuxHandle) Attach added in v1.35.0

func (h *ShellTmuxHandle) Attach() error

Attach creates a PTY connection to the shell's sibling tmux session. Idempotent: returns nil if already attached. This must be called before GetPTY() returns a usable file.

func (*ShellTmuxHandle) Close added in v1.35.0

func (h *ShellTmuxHandle) Close() error

Close stops the shell, kills the sibling tmux session, closes the PTY, and reaps the attach process to prevent zombies (matches TmuxSession.Close() pattern).

func (*ShellTmuxHandle) DoesSessionExist added in v1.35.0

func (h *ShellTmuxHandle) DoesSessionExist() bool

DoesSessionExist checks if the sibling tmux session is still present.

func (*ShellTmuxHandle) ExitCode added in v1.35.0

func (h *ShellTmuxHandle) ExitCode() (int, bool)

ExitCode queries the exit status of the shell process after it exits. Uses tmux display-message to read #{pane_dead_status}. Returns (exitCode, true) if the shell has exited, (0, false) if still running or session gone.

func (*ShellTmuxHandle) GetPTY added in v1.35.0

func (h *ShellTmuxHandle) GetPTY() (*os.File, error)

GetPTY returns the PTY file for reading terminal output from the shell. Returns an error if Attach() has not been called or the handle is closed.

func (*ShellTmuxHandle) Resize added in v1.35.0

func (h *ShellTmuxHandle) Resize(cols, rows int) error

Resize updates the PTY window dimensions by running tmux resize-window.

func (*ShellTmuxHandle) Spawn added in v1.35.0

func (h *ShellTmuxHandle) Spawn(workDir, command string) error

Spawn creates a new independent sibling tmux session running the given command in workDir. It does NOT attach a PTY; call Attach() for streaming I/O.

Equivalent of: tmux new-session -d -s {sessionName} -c {workDir} -- /bin/sh -c {command}

type Socket added in v1.37.0

type Socket string

Socket identifies which tmux server a command targets. The zero value ("") means the real, shared default server. The only way to obtain a non-trivial Socket is through ResolveSocket -- holding one proves resolution (including test-mode isolation) already happened, so callers building tmux argv via Args never need to re-derive or re-check isolation themselves.

This is a plain newtype, not an opaque struct: many callers legitimately need the socket name as a string too (struct fields for UI display, log lines, equality checks against ""), and forcing a conversion at every one of those sites would fight the pattern instead of guiding it. Args is the one sanctioned way to turn a Socket into a tmux command's argv; see the tmuxsocketscope lint pass for the structural check that every tmux invocation's args flow through it (or ResolveSocket/prependSocket) instead of a hand-rolled "-L" literal.

func ResolveSocket added in v1.37.0

func ResolveSocket(explicit string) Socket

ResolveSocket is the single choke point between "the socket a caller asked for" and "the socket a tmux command actually targets." An explicit non-empty socket always passes through unchanged (real per-worktree/per-test isolation, or a caller intentionally targeting a specific server, is always honored). An empty socket -- historically "the real shared default socket" everywhere in this package, including in code that enumerates or kills ALL sessions on it (ReconcileOrphanedTmuxSessions, batchPaneActivity, health checks) -- resolves to a per-process isolated socket inside a `go test` binary instead.

Before this existed, "empty string" meant the real default socket unconditionally, so ANY test that ended up calling a tmux-touching code path (not just tests that intentionally exercise tmux) could enumerate and kill every real session on a developer's machine, including sessions from an entirely separate, currently running production stapler-squad process. That happened repeatedly in production incidents traced to nothing more than a `go test ./server/...` run elsewhere on the same machine. Every function below that builds a tmux invocation from a raw socket string must resolve it through here first -- there is intentionally no second, competing way to decide "which socket does this command target."

This is deliberately NOT gated behind an explicit opt-in flag on the destructive functions themselves (the previous fix for this class of bug): a flag can be forgotten at any new call site. Resolving centrally, once, at the boundary where a caller-supplied socket turns into a real tmux invocation means every existing and future caller is isolated automatically, with no per-call-site action required.

func (Socket) Args added in v1.37.0

func (s Socket) Args(args ...string) []string

Args prepends "-L <socket>" to args when s is a non-default socket, and returns args unchanged for the default server (matching production behavior: an unscoped call targets the real shared socket, exactly as before this isolation mechanism existed).

func (Socket) String added in v1.37.0

func (s Socket) String() string

String returns the socket name, or "" for the default server.

type TmuxServerReady added in v1.35.0

type TmuxServerReady struct{}

TmuxServerReady is a zero-size proof token returned by EnsureServerRunning. BuildRuntimeDeps requires it as its first parameter to enforce that the tmux server is running before any sessions are loaded — preventing cold-restore of processes that are still alive inside tmux.

func EnsureServerRunning added in v1.1.0

func EnsureServerRunning(serverSocket string) (TmuxServerReady, error)

EnsureServerRunning starts the tmux server if it is not already running. Uses exec.Command directly so it always runs regardless of circuit breaker state. Returns a TmuxServerReady token that callers must pass to BuildRuntimeDeps.

type TmuxServerRegistry added in v1.18.0

type TmuxServerRegistry struct {
	// contains filtered or unexported fields
}

TmuxServerRegistry maintains a single tmux control-mode connection to a tmux server and pushes session-lifecycle events into an in-memory map. Callers query the map directly instead of forking tmux subprocesses.

func GetServerRegistry added in v1.18.0

func GetServerRegistry(socket string) *TmuxServerRegistry

GetServerRegistry returns the singleton TmuxServerRegistry for the given socket. Creates and starts the registry on first call for each socket. Never call from init().

func NewTmuxServerRegistry added in v1.18.0

func NewTmuxServerRegistry(serverSocket string) *TmuxServerRegistry

NewTmuxServerRegistry creates a new registry for the given server socket. Call Start(ctx) to begin listening for events.

func (*TmuxServerRegistry) IsHealthy added in v1.18.0

func (r *TmuxServerRegistry) IsHealthy() bool

IsHealthy implements SessionExistenceChecker and SessionLister.

func (*TmuxServerRegistry) ListSessions added in v1.18.0

func (r *TmuxServerRegistry) ListSessions() map[string]bool

ListSessions implements SessionLister. Returns a copy of the live sessions map.

func (*TmuxServerRegistry) NotifySessionCreated added in v1.35.0

func (r *TmuxServerRegistry) NotifySessionCreated(name string)

NotifySessionCreated proactively marks a session as existing in the registry. Called by TmuxSession.start() after a new session is confirmed via list-sessions, so that DoesSessionExist() returns true before the async %session-created control-mode event is processed.

func (*TmuxServerRegistry) SessionExists added in v1.18.0

func (r *TmuxServerRegistry) SessionExists(name string) bool

SessionExists implements SessionExistenceChecker.

func (*TmuxServerRegistry) Start added in v1.18.0

func (r *TmuxServerRegistry) Start(ctx context.Context) error

Start launches the control-mode process and begins processing events. It bootstraps the session map from list-sessions before marking the registry healthy. The returned error is non-nil only when the initial setup fails in a way that makes a retry impossible.

func (*TmuxServerRegistry) Stop added in v1.18.0

func (r *TmuxServerRegistry) Stop()

Stop shuts down the registry and closes all pending subscriber channels.

func (*TmuxServerRegistry) SubscribePaneExit added in v1.18.0

func (r *TmuxServerRegistry) SubscribePaneExit(ctx context.Context, sessionName string) <-chan struct{}

SubscribePaneExit implements PaneExitSubscriber. The returned channel is closed when the named session/pane exits or when ctx is cancelled.

type TmuxSession

type TmuxSession struct {

	// ExtraEnv holds additional KEY=VALUE pairs to pass as -e flags to tmux new-session.
	// Used to inject per-session environment variables such as DISPLAY for VNC support.
	ExtraEnv []string
	// contains filtered or unexported fields
}

TmuxSession represents a managed tmux session

func NewTmuxSession

func NewTmuxSession(name string, program string) *TmuxSession

NewTmuxSession creates a new TmuxSession with the given name and program. The executor is wrapped with a CircuitBreakerExecutor for resilience.

func NewTmuxSessionFromExisting

func NewTmuxSessionFromExisting(exactSessionName string) *TmuxSession

NewTmuxSessionFromExisting creates a TmuxSession that wraps an existing tmux session by its exact name. Unlike other constructors, this does NOT add any prefix to the session name - it uses the name exactly as provided. This is used for external sessions discovered via mux socket monitoring that already have tmux sessions.

The session must already exist in tmux. Call AttachToExisting() after creation to establish the PTY connection.

func NewTmuxSessionWithDeps

func NewTmuxSessionWithDeps(name string, program string, ptyFactory PtyFactory, cmdExec executor.Executor) *TmuxSession

NewTmuxSessionWithDeps creates a new TmuxSession with provided dependencies for testing. WithRegistry(nil) is passed so DoesSessionExist() uses cmdExec (the mock) instead of the global TmuxServerRegistry, which connects to real tmux and would bypass the mock executor.

func NewTmuxSessionWithPrefix

func NewTmuxSessionWithPrefix(name string, program string, prefix string) *TmuxSession

NewTmuxSessionWithPrefix creates a new TmuxSession with a custom prefix for process isolation. The executor is wrapped with a CircuitBreakerExecutor for resilience.

func NewTmuxSessionWithServerSocket

func NewTmuxSessionWithServerSocket(name string, program string, prefix string, serverSocket string, opts ...TmuxSessionOption) *TmuxSession

NewTmuxSessionWithServerSocket creates a new TmuxSession with complete server isolation. This uses the tmux -L flag to create a completely separate tmux server, providing true isolation from other tmux sessions. Use this for testing or when you need complete separation from production tmux sessions.

serverSocket: unique socket name (e.g., "test", "teatest_123", "isolated") prefix: session name prefix (e.g., "staplersquad_test_")

func (*TmuxSession) Attach

func (t *TmuxSession) Attach() (chan struct{}, error)

func (*TmuxSession) AttachToExisting

func (t *TmuxSession) AttachToExisting() error

AttachToExisting connects to an already-running tmux session and establishes the PTY connection. This is similar to RestoreWithWorkDir but assumes the session definitely exists. Returns an error if the session doesn't exist or PTY connection fails.

func (*TmuxSession) CapturePaneContent

func (t *TmuxSession) CapturePaneContent() (string, error)

CapturePaneContent captures the content of the tmux pane. When STAPLER_SQUAD_CM_COMMANDS=true and control mode is running, the query is sent over the control mode stdin pipe (zero new subprocesses); otherwise falls back to subprocess.

func (*TmuxSession) CapturePaneContentRaw

func (t *TmuxSession) CapturePaneContentRaw() (string, error)

CapturePaneContentRaw captures the pane content with ANSI codes preserved and WITHOUT joining wrapped lines. This is essential for hybrid streaming where we need to preserve exact cursor positioning. The -J flag (join wrapped lines) strips cursor positioning codes, breaking TUI rendering.

func (*TmuxSession) CapturePaneContentWithOptions

func (t *TmuxSession) CapturePaneContentWithOptions(start, end string) (string, error)

CapturePaneContentWithOptions captures the pane content with additional options. start and end specify the starting and ending line numbers (use "-" for the start/end of history).

func (*TmuxSession) Close

func (t *TmuxSession) Close() error

Close terminates the tmux session and cleans up resources

func (*TmuxSession) Detach

func (t *TmuxSession) Detach()

Detach disconnects from the current tmux session. It panics if detaching fails. At the moment, there's no way to recover from a failed detach.

func (*TmuxSession) DetachSafely

func (t *TmuxSession) DetachSafely() error

DetachSafely disconnects from the current tmux session without panicking

func (*TmuxSession) DoesSessionExist

func (t *TmuxSession) DoesSessionExist() bool

func (*TmuxSession) DoesSessionExistNoCache

func (t *TmuxSession) DoesSessionExistNoCache() bool

DoesSessionExistNoCache checks if session exists WITHOUT using the time-based cache. This is used for critical validation before session creation to ensure we have the most up-to-date information about session existence.

Concurrent callers (health checker, hibernation sweeper, the session-create retry loop, bulk instance restore, etc.) are coalesced via noCacheSF into a single in-flight subprocess — this keeps the "always fresh" contract (no TTL) while eliminating the redundant concurrent tmux subprocess spawns that used to hit an already-serialized single-threaded tmux server one-for-one with callers.

func (*TmuxSession) ExitStatus added in v1.37.0

func (t *TmuxSession) ExitStatus() (code int, signal string, ok bool)

ExitStatus reports the wrapped program's exit code and signal for a dead pane, via tmux's #{pane_dead_status}/#{pane_dead_signal} (populated by remain-on-exit). Returns ok=false if the pane is still alive, the session is already gone, or the pane never went through a dead state (nothing to report). Callers should read this as early as possible after detecting an exit -- the pane is destroyed the moment anything issues kill-session/respawn-pane against it, and this data goes with it.

func (*TmuxSession) FilterBanners

func (t *TmuxSession) FilterBanners(content string) (filteredContent string, bannersRemoved int)

FilterBanners removes tmux status banners from terminal output. This is useful for processing terminal output while excluding tmux status lines.

func (*TmuxSession) GetCursorPosition

func (t *TmuxSession) GetCursorPosition() (x, y int, err error)

GetCursorPosition returns the current cursor position in the tmux pane. Returns cursor X (column) and Y (row) coordinates, both 0-based.

func (*TmuxSession) GetPTY

func (t *TmuxSession) GetPTY() (*os.File, error)

GetPTY returns the PTY file descriptor for reading terminal output. This provides direct access to the PTY master for terminal streaming. Returns an error if the PTY is not initialized.

func (*TmuxSession) GetPaneCurrentPath

func (t *TmuxSession) GetPaneCurrentPath() (string, error)

GetPaneCurrentPath returns the current working directory of the tmux pane. This is used by CaptureCurrentState to persist cwd before shutdown for cold restore.

func (*TmuxSession) GetPaneDimensions

func (t *TmuxSession) GetPaneDimensions() (width, height int, err error)

GetPaneDimensions returns the current dimensions of the tmux pane. Returns width (columns) and height (rows).

When STAPLER_SQUAD_CM_COMMANDS=true and control mode is running, the query is sent over the existing control mode stdin pipe (zero new subprocesses). Otherwise it falls back to the original subprocess path.

func (*TmuxSession) GetPanePID

func (t *TmuxSession) GetPanePID() (int32, error)

GetPanePID returns the PID of the foreground process in the pane. This is used by HistoryLinker to correlate open files with session records.

func (*TmuxSession) GetSanitizedName added in v1.15.0

func (t *TmuxSession) GetSanitizedName() string

GetSanitizedName returns the tmux session name as it appears in `tmux list-sessions`. Used for bulk reconciliation against ListAllSessions output.

func (*TmuxSession) HasMeaningfulContent

func (t *TmuxSession) HasMeaningfulContent(content string) bool

HasMeaningfulContent checks if the terminal output contains meaningful content (excluding tmux status banners). This is used to determine if the session has produced actual output versus just tmux status line updates.

func (*TmuxSession) HasUpdated

func (t *TmuxSession) HasUpdated() (updated bool, hasPrompt bool, content string)

HasUpdated checks if the tmux pane content has changed since the last tick. It also returns true if the tmux pane has a prompt for aider or claude code.

func (*TmuxSession) RefreshClient

func (t *TmuxSession) RefreshClient() error

RefreshClient sends a refresh signal to the tmux client, forcing the process running inside to redraw at current dimensions. This is critical after resizing to update cursor positions and line wrapping.

func (*TmuxSession) ResetExitOnce added in v1.15.0

func (t *TmuxSession) ResetExitOnce()

ResetExitOnce resets the exit callback so it can fire again after a session restart. Also clears intentionalStop so the next StopControlMode() correctly guards the callback. Resets control mode refcount/cmd/exited so a stale dead process left by a prior crash does not corrupt the next Start/Stop cycle. Call this before reusing a TmuxSession object for a restarted session.

func (*TmuxSession) Restore

func (t *TmuxSession) Restore() error

Restore attaches to an existing session and restores the window size

func (*TmuxSession) RestoreWithWorkDir

func (t *TmuxSession) RestoreWithWorkDir(workDir string) error

func (*TmuxSession) SendInputViaControlMode added in v1.35.0

func (t *TmuxSession) SendInputViaControlMode(ctx context.Context, data []byte) error

SendInputViaControlMode sends raw bytes to the active pane through the already-open control mode connection. Uses the HIGH-PRIORITY queue so user keystrokes always jump ahead of any queued background operations (capture-pane, resize, etc.).

Fire-and-forget: enqueues the send-keys command and returns immediately without waiting for the tmux %begin/%end ack. The ack is consumed by the reader goroutine and discarded. This eliminates one CM round-trip from the interactive input path.

func (*TmuxSession) SendKeys

func (t *TmuxSession) SendKeys(keys string) (int, error)

func (*TmuxSession) SetDetachedSize

func (t *TmuxSession) SetDetachedSize(width, height int) error

SetDetachedSize set the width and height of the session while detached. This makes the tmux output conform to the specified shape.

func (*TmuxSession) SetExtraEnv added in v1.35.0

func (t *TmuxSession) SetExtraEnv(env []string)

SetExtraEnv sets additional KEY=VALUE environment variable pairs to inject via tmux new-session -e flags. Must be called before Start().

func (*TmuxSession) SetOnExitCallback added in v1.15.0

func (t *TmuxSession) SetOnExitCallback(fn func(reason string))

SetOnExitCallback registers a function called when the session exits unexpectedly. The callback fires at most once per TmuxSession lifetime (guarded by sync.Once). It is NOT called when StopControlMode() is the cause of the exit. The callback must not be called while the owning Instance's mu is held.

func (*TmuxSession) SetWindowSize

func (t *TmuxSession) SetWindowSize(cols, rows int) error

SetWindowSize allows external callers (like web UI) to set terminal dimensions. This is particularly useful for web terminal integration where the browser controls the size. This method executes the resize immediately by calling both PTY and tmux resize commands.

func (*TmuxSession) Start

func (t *TmuxSession) Start(workDir string) error

Start creates and starts a new tmux session, then attaches to it. Program is the command to run in the session (ex. claude). workdir is the git worktree directory.

func (*TmuxSession) StartControlMode

func (t *TmuxSession) StartControlMode() error

StartControlMode begins streaming terminal output via tmux control mode (-C flag). This is the proper way to get real-time terminal output from tmux, replacing pipe-pane + FIFO. Control mode provides structured notifications (%output, %session-changed, etc.) via stdout.

Benefits over pipe-pane: - No FIFO complexity or EOF issues - Direct protocol communication with tmux - Structured, parseable output format - Real-time notifications (no polling) - Native tmux feature (not a hack)

See: https://github.com/tmux/tmux/wiki/Control-Mode

func (*TmuxSession) StartWithCleanup

func (t *TmuxSession) StartWithCleanup(workDir string) (CleanupFunc, error)

StartWithCleanup creates and starts a new tmux session and returns a cleanup function. Usage: cleanup, err := session.StartWithCleanup(workDir); if err == nil { defer cleanup() }

func (*TmuxSession) StopControlMode

func (t *TmuxSession) StopControlMode() error

StopControlMode stops the control mode streaming and cleans up resources. With refcounting, this only actually stops the underlying process when the last caller disconnects. Intermediate callers decrement the refcount and return early.

func (*TmuxSession) SubscribeToControlModeUpdates

func (t *TmuxSession) SubscribeToControlModeUpdates() (string, chan []byte)

SubscribeToControlModeUpdates registers a new subscriber for real-time terminal output. Returns a subscriber ID and a channel that receives terminal output bytes. The channel has a buffer of 100 messages to handle burst traffic.

func (*TmuxSession) TapDAndEnter

func (t *TmuxSession) TapDAndEnter() error

TapDAndEnter sends 'D' followed by an enter keystroke to the tmux pane.

func (*TmuxSession) TapEnter

func (t *TmuxSession) TapEnter() error

TapEnter sends an enter keystroke to the tmux pane.

func (*TmuxSession) UnsubscribeFromControlModeUpdates

func (t *TmuxSession) UnsubscribeFromControlModeUpdates(subscriberID string)

UnsubscribeFromControlModeUpdates removes a subscriber and closes its channel.

type TmuxSessionOption added in v1.18.0

type TmuxSessionOption func(*TmuxSession)

TmuxSessionOption is a functional option for TmuxSession construction.

func WithRegistry added in v1.18.0

WithRegistry injects a SessionExistenceChecker; used in tests to avoid the global GetServerRegistry accessor. Passing nil suppresses the automatic GetServerRegistry call so no reconnect loop is started.

type TmuxStatePort added in v1.18.0

type TmuxStatePort interface {
	SessionExistenceChecker
	SessionLister
	PaneExitSubscriber
}

TmuxStatePort is the full registry interface.

type ZombieInfo added in v1.24.0

type ZombieInfo struct {
	PID     int
	PPID    int
	Command string
}

ZombieInfo describes a detected zombie process.

func ScanZombies added in v1.24.0

func ScanZombies() ([]ZombieInfo, error)

ScanZombies returns zombie processes (state "Z") that are direct children of the current process. Only direct children can be reaped via Wait4(-1, WNOHANG), so reporting system-wide zombies would produce un-reapable noise.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL