Documentation
¶
Index ¶
- Constants
- Variables
- func AcquireExecSlot(ctx context.Context, serverSocket string) (release func(), err error)
- func AcquireInputExecSlot(ctx context.Context, serverSocket string) (release func(), err error)
- func AcquireResyncExecSlot(ctx context.Context, serverSocket string) (release func(), err error)
- func BatchPaneDeadStatus(serverSocket string) (map[string]PaneDeadStatus, error)
- func Binary() string
- func ClampWinsizeDim(v int) uint16
- func CleanupSessions(cmdExec executor.Executor) error
- func CleanupSessionsOnServer(cmdExec executor.Executor, serverSocket string) error
- func CreateKeepaliveSession(serverSocket string) error
- func HostKeyFingerprint(key ssh.PublicKey) string
- func IsServerDown(serverSocket string) bool
- func KillOrphanedControlModeClients(serverSocket string) (int, error)
- func ListAllSessions(serverSocket string) (map[string]bool, error)
- func LookupChildPID(pid int) (description string, startedAt time.Time, ok bool)
- func NewTmuxSessionWithCleanup(name string, program string, opts ...TmuxSessionOption) (*TmuxSession, CleanupFunc)
- func NewTmuxSessionWithPrefixAndCleanup(name string, program string, prefix string, opts ...TmuxSessionOption) (*TmuxSession, CleanupFunc)
- func NewTmuxSessionWithServerSocketAndCleanup(name string, program string, prefix string, serverSocket string, ...) (*TmuxSession, CleanupFunc)
- func RecordZombieProcess(pid int, sessionName string, warnFn func(string, ...any))
- func RegisterControlModeMetrics() error
- func RegisterExecGateMetrics() error
- func RegisterForkPressureAlert(fn AlertFunc)
- func RemoveServerRegistry(socket string)
- func SetExitEmpty(serverSocket string, enabled bool) error
- func SetServerRecoveryCallback(fn func())
- func SetSubreaper() error
- func StartForkPressureLogger(ctx context.Context, interval time.Duration, logFn func(string, ...any), ...)
- func StartZombieReaper(ctx context.Context, interval time.Duration, logFn func(string, ...any), ...)
- func StartZombieWatcher(ctx context.Context, interval time.Duration, warnFn func(string, ...any), ...)
- func StopServerRegistry(socket string)
- func ToStaplerSquadTmuxName(str string) string
- func TrackChildPID(pid int, description string)
- func TryAcquireExecSlot(serverSocket string) (release func(), ok bool)
- func UntrackChildPID(pid int)
- func ValidateWorkDir(workDir string) error
- func WrapRemoteCommand(name string, args []string) (string, []string)
- type AlertFunc
- type BannerFilter
- type CleanupFunc
- type CommandRunner
- type ErrUnknownHostKey
- type ForkPressureLevel
- type ForkPressureStats
- type LocalRunner
- type MockCmdExec
- type PaneDeadStatus
- type PaneExitSubscriber
- type Pty
- type PtyFactory
- type PtySession
- type RemotePtyFactory
- type SSHClientPool
- func (p *SSHClientPool) Evict(name string, client *ssh.Client)
- func (p *SSHClientPool) GetOrDial(ctx context.Context, target SSHTarget, config *ssh.ClientConfig) (*ssh.Client, error)
- func (p *SSHClientPool) Peek(name string) (*ssh.Client, bool)
- func (p *SSHClientPool) RefCount(name string) int
- func (p *SSHClientPool) Release(name string)
- func (p *SSHClientPool) Remove(name string) error
- func (p *SSHClientPool) Subscribe(name string) (<-chan *ssh.Client, func())
- type SSHPtyFactory
- type SSHRunner
- func (r *SSHRunner) Dial(ctx context.Context) error
- func (r *SSHRunner) IsRemote() bool
- func (r *SSHRunner) Run(ctx context.Context, dir, name string, args ...string) ([]byte, error)
- func (r *SSHRunner) Start(ctx context.Context, dir, name string, args ...string) (io.WriteCloser, io.ReadCloser, func() error, error)
- type SSHRunnerOption
- type SSHTarget
- type SessionExistenceChecker
- type SessionLister
- type SessionName
- type ShellTmuxHandle
- func (h *ShellTmuxHandle) Attach() error
- func (h *ShellTmuxHandle) Close() error
- func (h *ShellTmuxHandle) DoesSessionExist() bool
- func (h *ShellTmuxHandle) ExitCode() (int, bool)
- func (h *ShellTmuxHandle) GetPTY() (*os.File, error)
- func (h *ShellTmuxHandle) Resize(cols, rows int) error
- func (h *ShellTmuxHandle) Spawn(workDir, command string) error
- type Socket
- type TmuxServerReady
- type TmuxServerRegistry
- func (r *TmuxServerRegistry) IsHealthy() bool
- func (r *TmuxServerRegistry) ListSessions() map[string]bool
- func (r *TmuxServerRegistry) NotifySessionClosed(name string)
- func (r *TmuxServerRegistry) NotifySessionCreated(name string)
- func (r *TmuxServerRegistry) SessionExists(name string) bool
- func (r *TmuxServerRegistry) SetFastRecheckWaitStartHook(hook func(backoff time.Duration))
- func (r *TmuxServerRegistry) Start(ctx context.Context) error
- func (r *TmuxServerRegistry) Stop()
- func (r *TmuxServerRegistry) SubscribePaneExit(ctx context.Context, sessionName string) <-chan struct{}
- type TmuxSession
- func NewTmuxSession(name string, program string, opts ...TmuxSessionOption) *TmuxSession
- func NewTmuxSessionFromExisting(exactSessionName string, opts ...TmuxSessionOption) *TmuxSession
- func NewTmuxSessionFromExistingWithServerSocket(exactSessionName string, serverSocket string, opts ...TmuxSessionOption) *TmuxSession
- func NewTmuxSessionWithDeps(name string, program string, ptyFactory PtyFactory, cmdExec executor.Executor, ...) *TmuxSession
- func NewTmuxSessionWithPrefix(name string, program string, prefix string, opts ...TmuxSessionOption) *TmuxSession
- func NewTmuxSessionWithServerSocket(name string, program string, prefix string, serverSocket string, ...) *TmuxSession
- func (t *TmuxSession) Attach() (chan struct{}, error)
- func (t *TmuxSession) AttachArgs() []string
- func (t *TmuxSession) AttachToExisting() error
- func (t *TmuxSession) CapturePaneContent() (string, error)
- func (t *TmuxSession) CapturePaneContentContext(ctx context.Context) (string, error)
- func (t *TmuxSession) CapturePaneContentPriority() (string, error)
- func (t *TmuxSession) CapturePaneContentRaw() (string, error)
- func (t *TmuxSession) CapturePaneContentRawPriority(ctx context.Context) (string, error)
- func (t *TmuxSession) CapturePaneContentWithOptions(start, end string) (string, error)
- func (t *TmuxSession) Close() error
- func (t *TmuxSession) Detach()
- func (t *TmuxSession) DetachSafely() error
- func (t *TmuxSession) DoesSessionExist() bool
- func (t *TmuxSession) DoesSessionExistNoCache() bool
- func (t *TmuxSession) EnsureRemoteSession(ctx context.Context, workDir string) error
- func (t *TmuxSession) ExitStatus() (code int, signal string, ok bool)
- func (t *TmuxSession) FilterBanners(content string) (filteredContent string, bannersRemoved int)
- func (t *TmuxSession) GetCursorPosition() (x, y int, err error)
- func (t *TmuxSession) GetPTY() (*os.File, error)
- func (t *TmuxSession) GetPaneCurrentPath() (string, error)
- func (t *TmuxSession) GetPaneDimensions() (width, height int, err error)
- func (t *TmuxSession) GetPaneDimensionsPriority(ctx context.Context) (width, height int, err error)
- func (t *TmuxSession) GetPanePID() (int32, error)
- func (t *TmuxSession) GetSanitizedName() string
- func (t *TmuxSession) HasMeaningfulContent(content string) bool
- func (t *TmuxSession) HasUpdated() (updated bool, hasPrompt bool, content string)
- func (t *TmuxSession) RefreshClient() error
- func (t *TmuxSession) RefreshClientPriority(ctx context.Context) error
- func (t *TmuxSession) ResetExitOnce()
- func (t *TmuxSession) Restore() error
- func (t *TmuxSession) RestoreWithWorkDir(workDir string) error
- func (t *TmuxSession) SendInputViaControlMode(ctx context.Context, data []byte) error
- func (t *TmuxSession) SendKeys(keys string) (int, error)
- func (t *TmuxSession) SetDetachedSize(width, height int) error
- func (t *TmuxSession) SetExtraEnv(env []string)
- func (t *TmuxSession) SetOnExitCallback(fn func(reason string))
- func (t *TmuxSession) SetWindowSize(cols, rows int) error
- func (t *TmuxSession) Start(workDir string) error
- func (t *TmuxSession) StartControlMode() error
- func (t *TmuxSession) StartWithCleanup(workDir string) (CleanupFunc, error)
- func (t *TmuxSession) StopControlMode() error
- func (t *TmuxSession) SubscribeToControlModeUpdates() (string, chan []byte)
- func (t *TmuxSession) TapDAndEnter() error
- func (t *TmuxSession) TapEnter() error
- func (t *TmuxSession) UnsubscribeFromControlModeUpdates(subscriberID string)
- type TmuxSessionOption
- type TmuxStatePort
- type ZombieInfo
Constants ¶
const ( // DialTimeout bounds a single SSH dial-and-handshake attempt (TCP // connect + SSH handshake, including host-key verification). DialTimeout = 10 * time.Second // ExistenceCheckTimeout bounds low-latency, existence-check-class // remote commands (e.g. "tmux has-session"). ExistenceCheckTimeout = 3 * time.Second // LongRunningCommandTimeout bounds longer remote operations (e.g. "git // worktree add" against a fresh clone). LongRunningCommandTimeout = 60 * time.Second )
Per-operation-class timeout budgets, per research/pitfalls.md §4. These bound SSHRunner's own dial step internally (DialTimeout); the remaining two are named constants for Epic 2.3's call sites (session/tmux/tmux.go, session/git/worktree_git.go) to build their per-RPC ctx from when they migrate onto SSHRunner -- SSHRunner.Run/Start themselves race every blocking SSH call against whatever ctx the caller supplies rather than imposing a second, redundant timeout of their own (see Run/Start's doc comments).
const LegacyTmuxPrefix = "claudesquad_"
const ProgramAider = "aider"
const ProgramClaude = "claude"
const ProgramGemini = "gemini"
const ResyncFastLaneTimeout = 3 * time.Second
ResyncFastLaneTimeout bounds the ENTIRE runFastLaneSubprocess call — both the exec-gate acquire wait and the subprocess execution that follows it — not just the acquire step runGatedFastLane's own ctx parameter bounds. 2026-08-25 incident: CapturePaneContentPriority/CapturePaneContentRawPriority each hand-built their own context.WithTimeout(context.Background(), defaultCapturePaneTimeout) — 10s, sized for the unrelated default pool — and RefreshClientPriority had no bound on its subprocess call at all (context.Background(), built via the context-less buildTmuxCommand rather than buildTmuxCommandContext). A capture that took a few seconds under real load (confirmed via debug tracing: the exec-gate slot itself was acquired in 0ms, so the delay was entirely in the subprocess call, not gate contention) stayed well within 10s and so never errored — but the client's own RESYNC_STALL_TIMEOUT_MS (4s, useVisibilityResync.ts) had already given up and force-disconnected, so the slow response was wasted work landing on a closed connection. Mirrors resyncFastLaneAcquireTimeout's margin logic: 3s leaves a 1s safety margin under the 4s client ceiling for network/ marshal/dispatch latency after the call returns.
const TmuxPrefix = "staplersquad_"
Variables ¶
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") )
var ErrEnsureRemoteSessionRequiresRemoteRunner = errors.New("EnsureRemoteSession requires a remote CommandRunner")
ErrEnsureRemoteSessionRequiresRemoteRunner is returned by EnsureRemoteSession when t's CommandRunner is not remote. The local session-creation path (start(), reached via Start/StartWithCleanup) already implements the equivalent existence-check-then-create flow against t.cmdExec/PTY machinery; EnsureRemoteSession exists specifically for the remote case, where the connection itself (not just the command) can drop mid-flight.
ErrRegistryUnavailable is returned when the registry is not healthy and cannot serve a request that requires the registry to be up.
var ErrSSHCircuitOpen = errors.New("ssh: circuit open, too many consecutive reconnect failures")
ErrSSHCircuitOpen is returned by SSHRunner when consecutive reconnect attempts to a remote host have exceeded the configured failure threshold. It is returned immediately, without attempting another dial, until the backoff interval elapses -- the mechanism that prevents a flaky network from turning into a tight redial loop that hammers the remote sshd.
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.
var ErrWorkDirMissing = errors.New("session working directory missing")
ErrWorkDirMissing indicates a session's working directory is unset or no longer exists on disk (e.g. a pruned git worktree). Callers can match on it with errors.Is to distinguish a permanent failure — the session should be failed with a clear status, not silently retried against a guessed directory.
Functions ¶
func AcquireExecSlot ¶ added in v1.37.0
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 AcquireInputExecSlot ¶ added in v1.47.0
AcquireInputExecSlot blocks until a slot in the input fast-lane pool is free, or ctx is done. It draws from a pool entirely separate from AcquireExecSlot's default pool — keyed on serverSocket+"#input" rather than serverSocket — sized by InputFastLaneSlotsOrDefault() rather than TmuxExecGate.SlotsOrDefault(). This keeps user keystrokes (the legacy per-keystroke send-keys path) from queuing behind a poller's capture-pane traffic on the shared default pool. Saturating one pool never blocks or borrows capacity from the other. release must be called exactly once, after the subprocess this slot guards has fully exited, same contract as AcquireExecSlot.
func AcquireResyncExecSlot ¶ added in v1.44.0
AcquireResyncExecSlot blocks until a slot in the resync fast-lane pool is free, or ctx is done. It draws from a pool that is entirely separate from AcquireExecSlot's default pool — keyed on serverSocket+"#resync" rather than serverSocket — sized by ResyncFastLaneSlotsOrDefault() rather than TmuxExecGate.SlotsOrDefault(). Saturating one pool never blocks or borrows capacity from the other. release must be called exactly once, after the subprocess this slot guards has fully exited, same contract as AcquireExecSlot.
func BatchPaneDeadStatus ¶ added in v1.47.0
func BatchPaneDeadStatus(serverSocket string) (map[string]PaneDeadStatus, error)
BatchPaneDeadStatus returns pane-dead status for every session on serverSocket in a single tmux invocation, keyed by session name. Intended for bulk health checks (SessionHealthChecker) that would otherwise issue one `display-message` subprocess call per session per tick -- see session/health.go's checkInstances, which groups instances by socket and calls this once per socket per tick instead of once per session. Returns ErrServerDown when the tmux server is not running.
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 ClampWinsizeDim ¶ added in v1.51.0
ClampWinsizeDim clamps a terminal dimension (columns or rows) coming from a resize request (ultimately client-controlled, e.g. a browser's terminal widget) to the range representable by pty.Winsize's uint16 fields ([1, 65535]) before the narrowing int->uint16 conversion, so an out-of-range or negative value can't silently wrap around into a bogus terminal size. Exported so session/instance_tmux.go and session/native_process_manager.go (which build pty.Winsize themselves, outside this package) share one implementation instead of duplicating it.
func CleanupSessions ¶
CleanupSessions kills all tmux sessions that start with "session-" on the default server
func CleanupSessionsOnServer ¶
CleanupSessionsOnServer kills all tmux sessions that start with "session-" on a specific server serverSocket: socket name for server isolation, empty string for default server
The two safeexec.CommandContext calls below are deliberately left off the CommandRunner seam: both feed the caller-injected cmdExec (executor.Executor) for test-injection/circuit-breaking, the same pre-existing, orthogonal seam documented on buildTmuxCommandContext -- not the local/remote-host seam this phase introduces.
func CreateKeepaliveSession ¶ added in v1.1.0
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 HostKeyFingerprint ¶ added in v1.47.0
HostKeyFingerprint returns the SSH host key fingerprint (SHA256, OpenSSH's default presentation format, e.g. "SHA256:abcd...") for key, suitable for display to a user deciding whether to trust a previously-unseen host.
func IsServerDown ¶ added in v1.15.0
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 KillOrphanedControlModeClients ¶ added in v1.41.0
KillOrphanedControlModeClients terminates every control-mode ("-C") client already attached to the tmux server at the moment this is called. Safe only at process startup, before any session has (re)started its own control mode: a freshly-started process cannot have spawned a control-mode client yet, so any control-mode client already attached is necessarily a leftover from a previous process instance that --tmux-keep-server intentionally kept alive across the restart (see docs/bugs/open/BUG-042-orphaned-control-mode-clients-overload-tmux-server.md). Left unreconciled, these accumulate one per restart and eventually crash the tmux server outright. Plain (non-control-mode) attach-session clients are left alone -- those can be real interactive users and are not this app's to manage.
func ListAllSessions ¶ added in v1.15.0
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
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, opts ...TmuxSessionOption) (*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, opts ...TmuxSessionOption) (*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, opts ...TmuxSessionOption) (*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
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 RegisterControlModeMetrics ¶ added in v1.52.0
func RegisterControlModeMetrics() error
RegisterControlModeMetrics registers the tmux_control_mode_* instruments against telemetry.GetMeter(). Idempotent via sync.Once — package init already calls this once; exported so a test can call it again safely.
func RegisterExecGateMetrics ¶ added in v1.52.0
func RegisterExecGateMetrics() error
RegisterExecGateMetrics registers the tmux_exec_gate_* instruments against telemetry.GetMeter(). Idempotent via sync.Once — package init already calls this once; exported so a test can call it again safely.
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
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), wg *sync.WaitGroup)
StartForkPressureLogger starts a background goroutine that logs fork pressure stats periodically.
wg is joined by server.Server.Shutdown() (backlog item 81e82fee-9528-4dc9-a513-1040b4dee2ec) so shutdown blocks until this goroutine has fully exited, not just been signaled via ctx.Done() — see the join at server/server.go's Shutdown().
func StartZombieReaper ¶ added in v1.24.0
func StartZombieReaper(ctx context.Context, interval time.Duration, logFn func(string, ...any), wg *sync.WaitGroup)
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).
wg is joined by server.Server.Shutdown() (backlog item 81e82fee-9528-4dc9-a513-1040b4dee2ec) — see the note on StartForkPressureLogger in fork_metrics.go for the shutdown-join rationale.
func StartZombieWatcher ¶ added in v1.24.0
func StartZombieWatcher(ctx context.Context, interval time.Duration, warnFn func(string, ...any), wg *sync.WaitGroup)
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.
wg is joined by server.Server.Shutdown() (backlog item 81e82fee-9528-4dc9-a513-1040b4dee2ec) — see the note on StartForkPressureLogger in fork_metrics.go for the shutdown-join rationale.
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 ¶
ToStaplerSquadTmuxName converts a string to a valid tmux session name with the default prefix
func TrackChildPID ¶ added in v1.35.0
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
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.
func ValidateWorkDir ¶ added in v1.48.0
ValidateWorkDir rejects an empty or nonexistent working directory instead of letting a caller silently fall back to a guessed one (e.g. os.Getwd(), often $HOME for a long-running server). Exported for reuse by session/tymux's validateWorkDir wrapper.
func WrapRemoteCommand ¶ added in v1.47.0
WrapRemoteCommand is wrapRemoteCommand, exported for callers outside this package that build remote tmux invocations against a RemotePtyFactory/ CommandRunner directly -- e.g. session.Instance.GetPTYSession (Task 4.4.1d), which needs the exact same $TMUX-unset/$TERM-forced treatment for a remote "tmux attach-session" raw-PTY attach that startRemoteControlMode (this package) already applies to a remote "tmux -C attach-session".
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 CommandRunner ¶ added in v1.47.0
type CommandRunner interface {
// Run executes name with args to completion in dir (or the caller's own
// current directory if dir is "") and returns combined stdout+stderr
// output, matching the shape of safeexec.CommandContext(ctx, name,
// args...) with Dir set to dir, then .CombinedOutput().
Run(ctx context.Context, dir, name string, args ...string) ([]byte, error)
// Start begins a persistent, piped invocation of name with args in dir
// (or the caller's own current directory if dir is "") and returns its
// stdin/stdout plus a wait function that blocks until the process exits
// (mirroring (*exec.Cmd).Wait()).
Start(ctx context.Context, dir, name string, args ...string) (stdin io.WriteCloser, stdout io.ReadCloser, wait func() error, err error)
// IsRemote reports whether commands run on a different host than this
// process. LocalRunner always returns false.
IsRemote() bool
}
CommandRunner is the execution seam between session/tmux (and session/git, which imports and reuses this type for its own mutating git/gh call sites) and the host a command actually runs on. Every tmux/git subprocess invocation that can be expressed as "run this and get combined output" or "start this and talk to it over stdin/stdout" goes through a CommandRunner instead of calling safeexec.CommandContext directly, so that a future SSH-backed implementation (Phase 2 of the ssh-remote-workspaces project) can be substituted at TmuxSession/GitWorktree construction time with no change to any downstream method's signature or behavior. See project_plans/ssh-remote-workspaces/decisions/ADR-002-commandrunner-in-session-tmux.md for why this interface lives here (the original consumer package) rather than in a new freestanding package.
Run covers one-shot invocations (has-session, kill-session, list-sessions, git commit, gh pr view, etc.) where the caller wants buffered combined output. Start covers the one long-lived, piped case in this package: the tmux "-C" control-mode attach client, which needs stdin/stdout it can write to and read from incrementally rather than a single buffered result.
Both take a dir parameter mirroring (*exec.Cmd).Dir: the working directory the command should run in, or "" for the caller's own current directory. This is required for session/git's worktree-scoped git/gh invocations (every git/gh call in worktree_git.go sets cmd.Dir to the worktree path) and is a no-op ("") for session/tmux's server-level commands, which have no meaningful working directory. dir and name are adjacent same-typed strings (see the `primitive-obsession-checklist` skill), but are left as plain positional parameters rather than wrapped in a newtype: unlike e.g. RepoRef's owner/repo (where a swap silently produces a different, still-valid repo), swapping dir and name here fails loudly and immediately at exec time (a directory path is never a valid program name and vice versa) rather than silently producing a plausible-but-wrong result — the specific harm newtypes in that checklist exist to prevent. See ADR-002's addendum for the full record of this decision.
IsRemote is the single mechanism code holding only a CommandRunner value (e.g. tmux.go, worktree_git.go) uses to branch on remoteness. It exists for the rare call site whose behavior cannot be expressed through Run/Start alone because it depends on OS-process-specific semantics (PID, signal delivery, exec.Cmd.Process) that have no SSH analog — see ADR-002's "Alternatives Considered" for why that surface is deliberately kept out of this interface rather than leaked into it.
type ErrUnknownHostKey ¶ added in v1.47.0
ErrUnknownHostKey is returned by SSHRunner.Dial (and, transitively, Run/Start's first call) when the configured HostKeyCallback reports the remote host's key as unknown -- never previously trusted -- rather than mismatched. SSHRunner never falls back to ssh.InsecureIgnoreHostKey(): an unknown host key stops the handshake and surfaces this typed error instead of silently connecting. Wraps the underlying HostKeyCallback error (e.g. a *knownhosts.KeyError) via Unwrap.
func (*ErrUnknownHostKey) Error ¶ added in v1.47.0
func (e *ErrUnknownHostKey) Error() string
func (*ErrUnknownHostKey) Unwrap ¶ added in v1.47.0
func (e *ErrUnknownHostKey) Unwrap() error
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 LocalRunner ¶ added in v1.47.0
type LocalRunner struct{}
LocalRunner runs commands as local OS subprocesses via safeexec.CommandContext. It is the zero-behavior-change default for every TmuxSession and GitWorktree today: swapping a direct safeexec.CommandContext(...) call site for LocalRunner{} changes nothing observable, since it wraps the exact same stdlib calls used before this seam existed.
func (LocalRunner) IsRemote ¶ added in v1.47.0
func (LocalRunner) IsRemote() bool
IsRemote implements CommandRunner. LocalRunner always runs on this host.
func (LocalRunner) Run ¶ added in v1.47.0
Run implements CommandRunner by wrapping safeexec.CommandContext(ctx, name, args...).CombinedOutput(), setting cmd.Dir to dir first (a no-op when dir is "").
func (LocalRunner) Start ¶ added in v1.47.0
func (LocalRunner) Start(ctx context.Context, dir, name string, args ...string) (io.WriteCloser, io.ReadCloser, func() error, error)
Start implements CommandRunner by wrapping safeexec.CommandContext(ctx, name, args...) with StdinPipe()/StdoutPipe() and Start(), returning a wait closure over Cmd.Wait(). cmd.Dir is set to dir first (a no-op when dir is "").
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)
type PaneDeadStatus ¶ added in v1.47.0
PaneDeadStatus reports whether a pane's wrapped program has exited (remain-on-exit placeholder), and its exit code/signal when it has.
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.
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 PtySession ¶ added in v1.47.0
type PtySession interface {
io.ReadWriteCloser
// Resize changes the PTY's terminal dimensions -- pty.Setsize locally,
// ssh.Session.WindowChange remotely (ssh-remote-workspaces Task 4.4.1e).
Resize(cols, rows int) error
}
PtySession abstracts a live, resizable, bidirectional pseudo-terminal connection -- the shared shape both a local raw-PTY attach (*os.File, via session's localPTYSession) and a remote SSH-backed attach (session/tmux/ssh_runner.go's sshPtySession, Task 4.4.1b) present to callers that only need to read/write terminal bytes and resize. It is deliberately NOT what PtyFactory.Start/StartWithSize return (*os.File) -- see PtyFactory's doc comment below for why that interface itself is untouched.
type RemotePtyFactory ¶ added in v1.47.0
type RemotePtyFactory interface {
// StartPty opens a new SSH channel on the remote host, requests a PTY of
// the given initial size, and starts name/args running attached to it
// (mirroring PtyFactory.StartWithSize's "size set before the child is
// forked" contract -- including reusing its *pty.Winsize parameter type,
// rather than adjacent cols/rows ints, for the same reason -- so a
// remote tmux attach-session never sees a 0x0 terminal either). dir is
// the remote working directory ("" for the login default), matching
// CommandRunner's dir semantics.
StartPty(ctx context.Context, ws *pty.Winsize, dir, name string, args ...string) (PtySession, error)
}
RemotePtyFactory creates a PtySession on a remote host over SSH -- the counterpart to PtyFactory for the raw (non-control-mode) PTY-attach path used by server/services/session_service.go's StreamTerminal raw-PTY fallback (ssh-remote-workspaces Phase 4, Task 4.4.1a). It is a new, additive interface rather than a retrofit of PtyFactory itself: PtyFactory.Start/StartWithSize's *os.File return type is baked into TmuxSession's ptmx field and dozens of call sites across tmux.go and control_mode.go (pty.Setsize, Fd()-based liveness checks, os.File-typed struct fields protected by ptmxMu) -- retrofitting that surface to an interface is out of scope for this epic's blast radius and would risk the local-only streaming paths this project must leave unmodified. See session/tmux/ssh_runner.go's SSHPtyFactory for the concrete RequestPty+Start-based implementation.
type SSHClientPool ¶ added in v1.47.0
type SSHClientPool struct {
// contains filtered or unexported fields
}
SSHClientPool is a reference-counted registry of one shared *ssh.Client per remote name, per the ssh-remote-workspaces plan's Design Decision (Epic 2.1): SSHRunner instances for the same remote share a single dialed connection, with each command/session opened as a new SSH *channel* on that connection rather than a new TCP+SSH handshake. Without this, a burst of concurrent session creation collides with sshd's default MaxStartups throttle (pre-mortem.md Failure #1, P1) -- see ssh_pool_test.go's load test for the empirical proof this pool is actually shared under a throttled listener.
Concurrent GetOrDial calls for the same not-yet-dialed target.Name coalesce onto a single in-flight dial via singleflight rather than racing independent dials. A pooled client is torn down only in two cases: an explicit Remove (the "remote-config removal" trigger) or a detected dead connection (Client.Wait() returning, spawned as a background watcher at register time) -- the last Release does NOT tear the client down, since the whole point of pooling is that the connection outlives any single caller's use of it.
func DefaultSSHClientPool ¶ added in v1.47.0
func DefaultSSHClientPool() *SSHClientPool
DefaultSSHClientPool returns the process-wide shared pool every SSHRunner uses unless constructed with WithSSHClientPool. Exported for callers outside this package that need to attach a SECOND consumer to the SAME pooled *ssh.Client a session's SSHRunner is already using -- e.g. session/sshremote.RemoteApprovalRelay, which must subscribe to the exact connection a remote session's terminal streaming already dialed (ADR-003) rather than dialing (and pooling) an unrelated one under the same RemoteName.
func NewSSHClientPool ¶ added in v1.47.0
func NewSSHClientPool() *SSHClientPool
NewSSHClientPool returns an empty pool.
func (*SSHClientPool) Evict ¶ added in v1.47.0
func (p *SSHClientPool) Evict(name string, client *ssh.Client)
Evict removes the pooled entry for name only if its current client is exactly client, force-closing it. A no-op if name has already been replaced by a newer dial (avoids evicting a fresh, unrelated connection out from under a concurrent caller). Used by SSHRunner when a session- level call fails in a way that indicates the pooled connection itself is dead, to accelerate the next redial rather than waiting on Client.Wait().
func (*SSHClientPool) GetOrDial ¶ added in v1.47.0
func (p *SSHClientPool) GetOrDial(ctx context.Context, target SSHTarget, config *ssh.ClientConfig) (*ssh.Client, error)
GetOrDial returns the shared *ssh.Client for target, dialing a new one (bounded by ctx) if none is pooled yet. Every call -- whether it triggers the dial or coalesces onto an in-flight/existing one -- increments target.Name's reference count by one; callers should pair this with a Release once they're done with the client for now.
func (*SSHClientPool) Peek ¶ added in v1.47.0
func (p *SSHClientPool) Peek(name string) (*ssh.Client, bool)
Peek returns the currently pooled client for name without dialing, reporting false if no live entry exists. Callers that already hold a reference (via a prior GetOrDial) can use this as a cheap hot-path check that does not touch the singleflight machinery or count as a dial attempt for backoff/circuit-breaker purposes.
func (*SSHClientPool) RefCount ¶ added in v1.47.0
func (p *SSHClientPool) RefCount(name string) int
RefCount reports the current reference count for name (0 if not pooled). Exposed for tests and observability, not for teardown decisions.
func (*SSHClientPool) Release ¶ added in v1.47.0
func (p *SSHClientPool) Release(name string)
Release decrements the reference count for name. It never closes the underlying client itself -- per the Design Decision, the last channel/session closing does not tear down the shared connection; only Remove (explicit remote-config removal) or a detected dead connection does.
func (*SSHClientPool) Remove ¶ added in v1.47.0
func (p *SSHClientPool) Remove(name string) error
Remove force-closes and evicts the pooled client for name regardless of reference count -- the explicit-remote-config-removal teardown trigger. A no-op returning nil if name has no pooled entry.
func (*SSHClientPool) Subscribe ¶ added in v1.47.0
func (p *SSHClientPool) Subscribe(name string) (<-chan *ssh.Client, func())
Subscribe returns a channel that receives name's pooled *ssh.Client every time register() installs one for it -- both the very first dial and every subsequent redial after an Evict/dead-connection cycle. This is the reconnect signal session/sshremote's RemoteApprovalRelay subscribes to in order to re-open its direct-streamlocal channel against a fresh connection after the underlying one drops and redials, per project_plans/ssh-remote-workspaces/implementation/plan.md Story 5.1.2 / Task 5.1.2a. Nothing in this package previously needed to observe a reconnect after the fact -- SSHRunner only ever calls GetOrDial again on-demand -- so this is new, minimal surface, not a pre-existing hook.
The returned channel is buffered (capacity 1) and lossy by design: a subscriber that hasn't drained the previous notification only cares about the MOST RECENT client, so a notify that finds a full channel drops the stale value and pushes the fresh one instead of blocking the notifier (register, called from GetOrDial's caller) or growing unbounded.
If a client is already pooled for name when Subscribe is called, the channel is primed with it immediately -- a subscriber that attaches after the first dial doesn't have to wait for the NEXT reconnect to learn about the current client.
The returned unsubscribe func must be called once the subscriber is done; it removes the channel from the pool's subscriber list so future notifies don't leak a send to a channel nobody reads anymore.
type SSHPtyFactory ¶ added in v1.47.0
type SSHPtyFactory struct {
// contains filtered or unexported fields
}
SSHPtyFactory implements RemotePtyFactory (session/tmux/pty.go, Task 4.4.1a) using an SSHRunner's pooled *ssh.Client: RequestPty followed by Start(cmd) gets a genuine remote pseudo-terminal running a specific command (session/tmux/tmux.go's local buildAttachCommand()+PtyFactory equivalent for "tmux attach-session -t name", not a bare login shell -- plan.md's Task 4.4.1b sketch names session.Shell(), but Shell() takes no command argument; Start(cmd) is the ssh package's primitive for "run this exact command with a PTY attached," which is what the raw-PTY-attach path needs). Used by server/services/session_service.go's StreamTerminal raw-PTY fallback for a remote session (control-mode's remote path, control_mode.go's StartControlMode, does NOT use this -- tmux control mode's protocol is plain text over stdin/stdout and needs no PTY, so it goes through CommandRunner.Start directly, same as this type's non-PTY sibling).
func NewSSHPtyFactory ¶ added in v1.47.0
func NewSSHPtyFactory(runner *SSHRunner) *SSHPtyFactory
NewSSHPtyFactory constructs an SSHPtyFactory over runner's pooled connection.
func (*SSHPtyFactory) StartPty ¶ added in v1.47.0
func (f *SSHPtyFactory) StartPty(ctx context.Context, ws *pty.Winsize, dir, name string, args ...string) (PtySession, error)
StartPty implements RemotePtyFactory. Mirrors SSHRunner.Run/Start's own dial/session/release bookkeeping (see those methods' doc comments for why a newSession failure doesn't evict the shared client or count against the reconnect backoff) but additionally requests a PTY before starting cmd, and sizes it BEFORE the command starts (RequestPty's rows/cols arguments, taken from ws -- the same *pty.Winsize type PtyFactory.StartWithSize takes, rather than adjacent cols/rows ints), matching PtyFactory.StartWithSize's "size set before the child is forked" contract so a remote tmux attach-session never briefly sees a 0x0 terminal and self-disconnects.
type SSHRunner ¶ added in v1.47.0
type SSHRunner struct {
// contains filtered or unexported fields
}
SSHRunner implements CommandRunner over a persistent, pooled *ssh.Client: Run and Start execute remote commands as new SSH channels on the shared connection for Target.Name (session/tmux/ssh_pool.go), rather than dialing a fresh TCP+SSH connection per call. It never uses ssh.InsecureIgnoreHostKey() -- callers supply a pre-built ssh.ClientConfig (Phase 3 constructs this from IdentityRef-resolved signer + KnownHostsStore; this package accepts it directly for testability, e.g. a knownhosts.New()-backed or fixed-key HostKeyCallback in tests).
func NewSSHRunner ¶ added in v1.47.0
func NewSSHRunner(target SSHTarget, config ssh.ClientConfig, opts ...SSHRunnerOption) *SSHRunner
NewSSHRunner constructs an SSHRunner for target, using config as the base SSH client configuration. config.HostKeyCallback is wrapped to translate an unknown-host outcome into ErrUnknownHostKey (see wrapHostKeyCallback); config.Config's KeyExchanges/Ciphers/MACs are overwritten with SSHRunner's pinned modern allowlist regardless of what config supplies, since pinning them explicitly -- not trusting either the package default or a caller's possibly-stale choice -- is the whole point of Task 2.1.1d.
func (*SSHRunner) Dial ¶ added in v1.47.0
Dial establishes (or reuses, via the shared pool) the SSH connection for this remote, bounded by DialTimeout nested inside ctx. Run and Start dial lazily on first use, so most callers never need this directly; it's exposed for callers that want to eagerly validate connectivity and host-key trust before doing real work (e.g. RemoteHealthProber, Epic 6.4, reusing this same pooled client for its liveness checks rather than opening a dedicated connection).
func (*SSHRunner) IsRemote ¶ added in v1.47.0
IsRemote implements CommandRunner. SSHRunner always runs commands on a different host than this process (paired with LocalRunner.IsRemote() returning false -- the single mechanism every "is this remote" check reads, per architecture-review.md Blocker 1 / command_runner.go's doc comment).
func (*SSHRunner) Run ¶ added in v1.47.0
Run implements CommandRunner.Run: opens a new session on the shared client and returns its combined stdout+stderr, matching CommandRunner.Run's contract. Every blocking SSH call along the way (client acquisition/dial, session open, CombinedOutput) is raced against ctx.Done(); on expiry the session is force-closed (safe -- sessions are per-call, never shared) and a context-deadline error is returned rather than blocking indefinitely.
A newSession failure here is NOT treated as connection death and does NOT evict the shared client: client.NewSession() can fail for reasons that say nothing about the underlying connection's health -- most importantly a per-connection channel-limit rejection (OpenSSH's default MaxSessions 10), which is an expected, benign condition at exactly the concurrency level ssh_pool_test.go's load test targets (15-20 concurrent sessions sharing one connection). Evicting on every such error would force-close the shared client out from under every other concurrently- active caller -- the connection-layer cascade failure the pool exists to prevent, reintroduced one layer up at the channel layer. Dead-connection detection has exactly one source of truth: the pool's own Client.Wait() background watcher (session/tmux/ssh_pool.go), which is unaffected by (and doesn't need help from) a single failed channel-open.
func (*SSHRunner) Start ¶ added in v1.47.0
func (r *SSHRunner) Start(ctx context.Context, dir, name string, args ...string) (io.WriteCloser, io.ReadCloser, func() error, error)
Start implements CommandRunner.Start: opens a new session on the shared client, wires its stdin/stdout as pipes, and starts cmd without waiting for it to complete, returning a wait func mirroring (*exec.Cmd).Wait(). session.Setenv is deliberately not used (most sshd AcceptEnv is disabled by default); environment normalization is the caller's job via the command line itself, not this method.
Only the session-open and the Start() call itself are raced against ctx.Done() -- once Start() has returned successfully, the process is running and wait() is meant to be called out-of-band by the caller later, exactly like LocalRunner.Start's cmd.Wait, so wait() is intentionally not ctx-bound here.
The pool reference client() acquires is released exactly once: on every early-error return path here, or -- once Start succeeds -- inside the returned wait func, the first time it's called. A caller that never calls wait() after a successful Start leaks that one reference (the same caveat LocalRunner.Start's cmd.Wait carries for reaping the OS process); there is currently no production caller of CommandRunner.Start to observe this against (control_mode.go, the one long-lived piped use case in this package, still talks to *exec.Cmd directly and explicitly rejects remote CommandRunners -- see its IsRemote check -- pending Epic 2.3's remote control-mode wiring).
Like Run, a newSession failure here does not evict the shared client or count against the reconnect backoff -- see Run's doc comment for why (a channel-open failure, e.g. a MaxSessions rejection, is not evidence the connection itself is dead; the pool's Client.Wait() watcher is the sole dead-connection-detection path).
type SSHRunnerOption ¶ added in v1.47.0
type SSHRunnerOption func(*SSHRunner)
SSHRunnerOption configures an SSHRunner at construction time.
func WithSSHClientPool ¶ added in v1.47.0
func WithSSHClientPool(pool *SSHClientPool) SSHRunnerOption
WithSSHClientPool overrides the pool an SSHRunner dials/shares connections through. Defaults to the process-wide defaultSSHClientPool; tests use this to inject an isolated pool per test.
type SSHTarget ¶ added in v1.47.0
type SSHTarget struct {
// Name is the pool/registry key -- every SSHRunner dialing the same
// remote must use the same Name for pooling to actually share a
// connection.
Name string
// Addr is the "host:port" dialed over TCP.
Addr string
}
SSHTarget identifies a single named SSH remote: a stable name to key the shared connection-pool entry by (corresponding to config.RemoteConfig.Name once Phase 3 wires remote configuration), plus the "host:port" address actually dialed over TCP. Bundled into one type rather than left as two adjacent strings (see the `primitive-obsession-checklist` skill): unlike CommandRunner's dir/name (which fail loudly and immediately at exec time if swapped -- a directory path is never a valid program name), swapping Name and Addr here compiles silently and both still "look like" plausible strings in the wrong slot, which is exactly the silent- plausible-wrongness a newtype exists to prevent.
type SessionExistenceChecker ¶ added in v1.18.0
SessionExistenceChecker answers "is session X alive right now?" Used by TmuxSession.DoesSessionExist to avoid exec.Command forks.
type SessionLister ¶ added in v1.18.0
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
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.
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) NotifySessionClosed ¶ added in v1.44.0
func (r *TmuxServerRegistry) NotifySessionClosed(name string)
NotifySessionClosed proactively marks a session as gone in the registry. Called by TmuxSession.Close() right after a synchronous "kill-session" subprocess confirms the session is dead, so DoesSessionExist()'s registry fast path returns false immediately instead of trusting a stale "exists" entry until the async %session-closed control-mode event is processed -- the symmetric counterpart of NotifySessionCreated's create-side fast path.
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) SetFastRecheckWaitStartHook ¶ added in v1.42.0
func (r *TmuxServerRegistry) SetFastRecheckWaitStartHook(hook func(backoff time.Duration))
SetFastRecheckWaitStartHook installs a callback invoked synchronously, on reconnectLoop's own goroutine, at the moment a wait's backoff reaches fastRecheckMinBackoff -- i.e. a wait that is about to run its fast-recheck attempts. It exists so tests can deterministically synchronize with the start of a fast-recheck window instead of estimating cycle timing; see TestTmuxServerRegistry_PaneExitDetectedDespiteElevatedBackoff in server_registry_integration_test.go for why the estimate approach was flaky.
The hook must not block: it runs inline on reconnectLoop's goroutine and delays the fast-recheck attempts themselves for as long as it runs. Pass nil to remove the hook (the default; safe in production since nothing else populates this field).
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, opts ...TmuxSessionOption) *TmuxSession
NewTmuxSession creates a new TmuxSession with the given name and program. The executor is wrapped with a CircuitBreakerExecutor for resilience. opts is trailing/variadic so existing call sites are unaffected; pass WithCommandRunner to inject a non-default CommandRunner (e.g. a remote-backed one in Phase 2, or a test spy).
func NewTmuxSessionFromExisting ¶
func NewTmuxSessionFromExisting(exactSessionName string, opts ...TmuxSessionOption) *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 NewTmuxSessionFromExistingWithServerSocket ¶ added in v1.41.0
func NewTmuxSessionFromExistingWithServerSocket(exactSessionName string, serverSocket string, opts ...TmuxSessionOption) *TmuxSession
NewTmuxSessionFromExistingWithServerSocket is like NewTmuxSessionFromExisting but targets an isolated tmux server socket (e.g. a shell session's TmuxServerSocket) instead of the default server. serverSocket is resolved through ResolveSocket, so test-mode isolation still applies when the caller passes "".
func NewTmuxSessionWithDeps ¶
func NewTmuxSessionWithDeps(name string, program string, ptyFactory PtyFactory, cmdExec executor.Executor, opts ...TmuxSessionOption) *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. Additional opts (e.g. WithCommandRunner) are applied after WithRegistry(nil), so callers can still override further.
func NewTmuxSessionWithPrefix ¶
func NewTmuxSessionWithPrefix(name string, program string, prefix string, opts ...TmuxSessionOption) *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) AttachArgs ¶ added in v1.47.0
func (t *TmuxSession) AttachArgs() []string
AttachArgs returns the tmux argv (socket flag + "attach-session -t name") this session's raw-PTY attach uses locally (buildAttachCommand) -- exposed so callers outside this package can build the equivalent remote attach via a RemotePtyFactory (session/tmux/pty.go, ssh-remote-workspaces Task 4.4.1d: server/services/session_service.go's StreamTerminal raw-PTY fallback for a remote session) without reaching into unexported fields.
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) CapturePaneContentContext ¶ added in v1.46.0
func (t *TmuxSession) CapturePaneContentContext(ctx context.Context) (string, error)
CapturePaneContentContext is CapturePaneContent with an external context threaded onto the subprocess call itself (not just the exec-gate wait), so a caller that cancels ctx can kill an already-running capture-pane process rather than only giving up on waiting for a free gate slot. Added for the SessionDriver polling path (session.PreviewContext), whose stop channel needs to interrupt a capture-pane call already in flight, not just abandon the wait for one — see session/session_driver.go's stop/join mechanism.
func (*TmuxSession) CapturePaneContentPriority ¶ added in v1.44.0
func (t *TmuxSession) CapturePaneContentPriority() (string, error)
CapturePaneContentPriority mirrors CapturePaneContent but routes the subprocess call through the resync exec-gate fast lane (runGatedFastLane) instead of the default pool (runGated), so resync-triggered captures don't queue behind ordinary tmux exec traffic on the same server socket (Epic 4.2, terminal:resync-exec-gate-fast-lane). It does not use the control-mode path, unlike CapturePaneContent — resync callers need the isolation the subprocess gate provides, and control mode has no gate to isolate against.
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) CapturePaneContentRawPriority ¶ added in v1.49.0
func (t *TmuxSession) CapturePaneContentRawPriority(ctx context.Context) (string, error)
CapturePaneContentRawPriority mirrors CapturePaneContentPriority but without -J, for the same reason CapturePaneContentRaw omits it from CapturePaneContent (see its doc comment): a fast-lane caller that will replay the result into a live terminal emulator needs tmux's own wrap points and cursor-positioning codes intact, not joined/stripped by -J.
ctx is caller-supplied — see runFastLaneSubprocess's doc comment: this is typically the last of several fast-lane calls handleCurrentPaneRequest makes for one resync (refresh-client x3, a dimension verify, then this), so it must share the same overall deadline as its siblings rather than get its own fresh ResyncFastLaneTimeout allowance.
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. Takes detachMutex (the established outer lock over ptmxMu, see ptmxMu's doc comment) for its whole body and permanently flips ptyClosed before running any cleanup, so a concurrent AttachToExisting()/RestoreWithWorkDir() racing this call can never install a PTY after teardown has already started -- tryInstallPTYTriple observes ptyClosed and refuses.
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) EnsureRemoteSession ¶ added in v1.47.0
func (t *TmuxSession) EnsureRemoteSession(ctx context.Context, workDir string) error
EnsureRemoteSession creates the remote tmux session t.sanitizedName over t.commandRunner() if it does not already exist, reusing it if it does. This is Story 2.3.2's existence-check-before-create logic in isolation: unlike start() (the local session-creation path), it does not set up a PTY, control mode, or any of the other local-session machinery -- wiring a remote TmuxSession into a full production session lifecycle is Phase 4's job (project_plans/ssh-remote-workspaces/implementation/plan.md), not this epic's.
The failure mode this closes (research/pitfalls.md §1): an SSH channel drop mid-command can be indistinguishable, from the caller's side, from the remote command itself failing -- the remote tmux new-session may have already succeeded even though Run() returned an error. A caller that blindly retries plain "new-session" on that basis would get "duplicate session" at best, or -- if the sanitized name were ever allowed to differ between attempts -- a genuine duplicate at worst.
This is NOT closed by "-A" (attach-if-exists) alone, despite that being the obvious-looking fix: "-A" attaches to an already-existing session by re-executing the client against it, which requires a PTY -- and SSHRunner.Run never requests one (no RequestPty call anywhere in ssh_runner.go, by design: Run is the one-shot "get combined output" case, not an interactive attach). Confirmed empirically (TestNewSessionA_AgainstExistingSession_FailsOverNonPTYChannel): "tmux new-session -A -d" against an already-existing session, run over a non-PTY SSH channel, fails with "open terminal failed: not a terminal" (exit status 1) -- it does not silently attach. So "-A" alone still leaves a caller-visible error in the exact race window this function exists to close: has-session reports absent, a concurrent creator (a prior dropped-connection retry, or a genuinely concurrent caller) wins before this call's own new-session -A runs, and that new-session -A then fails against the now-existing session for the PTY reason above.
The actual sequence, each command run through wrapRemoteCommand (Story 2.3.1) since t.commandRunner().IsRemote() is required to call this at all:
- An explicit "has-session" check first (remoteHasSession), so a caller can distinguish "reused" from "created" without depending on new-session's exit code, and so the common case (no race) never issues a doomed-to-fail new-session -A against an existing session.
- If absent, "new-session -A -d ..." (createRemoteSession). "-A" still matters here even though it can't silently attach over a non-PTY channel: without it, a race-losing new-session would fail with "duplicate session" -- a different, but equally real, tmux-side error -- so this is not "assume -A works and skip everything else," it's "keep -A anyway (correct if a PTY caller ever attaches through this path) and add the recheck this channel actually needs."
- If step 2 fails, one more remoteHasSession recheck before surfacing the error: if the session now exists, that failure was the race above, not a real creation failure, and is treated as success.
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) GetPaneDimensionsPriority ¶ added in v1.49.0
func (t *TmuxSession) GetPaneDimensionsPriority(ctx context.Context) (width, height int, err error)
GetPaneDimensionsPriority mirrors GetPaneDimensions' control-mode-first behavior, but routes its subprocess fallback through the resync exec-gate fast lane (with a caller-supplied, shared ctx — see CapturePaneContentRawPriority's doc comment) instead of the default pool with no subprocess-level bound at all. handleCurrentPaneRequest's resize-through-verify path calls GetPaneDimensions twice per resync (an initial check, then a post-resize verify); when control mode isn't available and it falls through to a subprocess, that call needs the same fast-lane isolation and shared deadline as the refresh/capture calls around it, not the unbounded default-pool 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) RefreshClientPriority ¶ added in v1.44.0
func (t *TmuxSession) RefreshClientPriority(ctx context.Context) error
RefreshClientPriority mirrors RefreshClient's Method 1 subprocess path but routes through the resync exec-gate fast lane (runGatedFastLane) instead of the default pool (Epic 4.2). Unlike RefreshClient, it does not attempt the control-mode path or the SIGWINCH fallback (Method 2) — resync's priority caller wants a bounded, fast-lane-only refresh, not the full fallback chain.
ctx is caller-supplied — see CapturePaneContentRawPriority's doc comment: handleCurrentPaneRequest calls this up to 3 times per resync, and all 3 must share one overall deadline rather than each getting its own fresh ResyncFastLaneTimeout allowance.
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) 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)
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
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 WithCommandRunner ¶ added in v1.47.0
func WithCommandRunner(r CommandRunner) TmuxSessionOption
WithCommandRunner injects a CommandRunner, overriding the LocalRunner{} default every constructor otherwise applies. Used to swap in a remote-backed CommandRunner (Phase 2 of ssh-remote-workspaces) or a test spy that records/controls what the session's subprocess calls do.
func WithRegistry ¶ added in v1.18.0
func WithRegistry(r SessionExistenceChecker) TmuxSessionOption
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
ZombieInfo describes a detected zombie process.
func ScanZombies ¶ added in v1.24.0
func ScanZombies(ctx context.Context) ([]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.
The `ps` subprocess is bounded to 10s, derived from ctx so that canceling ctx (e.g. StartZombieWatcher's caller signaling shutdown) aborts an in-flight `ps` call immediately instead of leaving the caller's select loop blocked inside ScanZombies for up to the full 10s even after shutdown was requested — this was the root cause of an intermittent goroutine-leak-looking failure in TestStartZombieWatcher_GoroutineFullyExits_When_WaitGroupIsJoined under load.
Source Files
¶
- banner_filter.go
- binary.go
- command_runner.go
- control_mode.go
- control_mode_observability.go
- exec_gate.go
- exec_gate_observability.go
- fork_metrics.go
- pty.go
- registry_port.go
- remote_env.go
- server_registry.go
- shell_handle.go
- ssh_pool.go
- ssh_runner.go
- ssh_runner_backoff.go
- subreaper_linux.go
- test_helpers.go
- tmux.go
- tmux_unix.go
- zombie_detector.go
- zombie_reaper.go