Help us shape the future of Go! Take the 2026 Go Developer Survey

procs

package
v1.0.2 Latest Latest
Warning

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

Go to latest
Published: Oct 14, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package procs finds and stops agent sessions started by a caller, for orphan cleanup after a crash.

Sessions are found two ways: the pid file the runner writes in each session directory, and a scan of the process table for processes running an agent executable (AgentExecutables) and carrying a session marker: the `--name agent-…` argument every claude session is started with, or the override that hands a codex session its session directory. A session in the container sandbox has a third: the agent runs in the container, so the container is what is found and stopped, from the id file and the label the runner leaves (see container.go); what the process table shows of it is the engine client that started it. When the caller supplies a host MCP server, that optional process is a fourth thing to stop, recorded in its own pid file.

An opencode or a pi session is found through its pid file alone. opencode is given its session directory through OPENCODE_CONFIG, an environment variable, so its argv carries no path-bearing token to scope a ps-scan match to one factory's state directory the way the claude and codex markers do, and reading a process's environment to recover it is not portable across the platforms this package runs on; pi renames its process as it starts, which on Linux and macOS overwrites the command line a ps scan would read, so it shows nothing but "pi". Either pid file is trusted because what the process table does show is an agent executable (AgentExecutables); a pid file naming anything else is a pid reused by an unrelated process and is deleted. A crashed session of either agent with no live pid file is not found by orphan cleanup.

The scan and the containers are scoped to one factory: a process only counts when its command line also references this state directory's sessions directory (a claude session's argv carries `--append-system-prompt-file <sessions dir>/<session>/system-prompt.md`, a codex session's the session directory in the same override, an engine client both the container's label and its id file), and a container only when the session directory its label carries lies under it, so neither reports a session of another project's factory, however many share a machine. A pid file is scoped by where it lies instead: nothing in an agent's command line ties it to a state directory, so the pid a reboot handed to another factory's opencode or pi session is trusted and stopped like the session the file was written for.

Finding a session is not free of consequence: the stale pid, container-id and server-pid files read along the way are deleted, which is the cleanup a caller stopping sessions wants. A caller that only inspects asks through a Finder with ReadOnly set, which reports the same sessions and leaves every file where it is.

A session in a Docker Sandbox (sbx) is found by neither the pid file, the ps scan nor the container engine: its agent runs inside the sandbox, a microVM the host's process table and docker ps know nothing of. Its own records, SandboxNameFile and SandboxWorkspaceFile, are cleaned up separately by CleanSandboxes and CleanSandboxDirs, not by Find: a caller runs one of them once at startup, before any session starts, so that every record found then belongs to a session an earlier process died without closing, never one still in use. Cleanup removes the recorded sandbox with `sbx rm --force <name>`, which drops the sandbox's network policy rules along with it — sbx removes those with the sandbox itself, so cleanup makes no separate `sbx policy rm` call — and it removes the recorded primary workspace, a directory tree under os.TempDir(), once that path passes the workspace path rule (validWorkspacePath): absolute, its parent the cleaned os.TempDir(), and its base name containing "sbx-primary-" with at least one character after the last occurrence. Each record is deleted only once the removal it names succeeds, so a failed removal is retried by a later call, and the two records are handled independently, so a session directory holding a workspace record but no sandbox-name record still has its workspace cleaned up.

Index

Constants

View Source
const CodexSessionMarker = "shell_environment_policy.set.SESSION_DIR="

CodexSessionMarker is the argv fragment that identifies a codex session using the default namespace. The shell environment override identifies the session even when the caller supplies no MCP servers.

View Source
const ContainerIDFile = "container-id"

ContainerIDFile is the file in a session directory the engine writes the container's id to (--cidfile) when a container-backed session starts. It is removed when the session ends, so a session directory holding one is a session whose container may still be running.

View Source
const ContainerLabel = "agent.session"

ContainerLabel is the label every container-backed session's container carries, with the session directory as its value, so `docker ps --filter label=agent.session` lists one factory's sessions and the value says which session each one is.

View Source
const DefaultGrace = 5 * time.Second

DefaultGrace is the time allowed for exit after SIGTERM before SIGKILL.

View Source
const PIDFile = "pid"

PIDFile is the file a running session's pid is written to.

View Source
const SandboxNameFile = "sandbox-name"

SandboxNameFile is the file in a session directory the runner writes the name of the Docker Sandbox (sbx) a sandbox-backed session runs in. It is removed with the sandbox when the session ends, so a session directory holding one is a session whose sandbox may still exist. CleanSandboxes and CleanSandboxDirs read it to find that sandbox and remove it with `sbx rm --force <name>`, which also drops the sandbox's network policy rules: sbx removes those with the sandbox itself, so cleanup makes no separate `sbx policy rm` call.

View Source
const SandboxWorkspaceFile = "sandbox-workspace"

SandboxWorkspaceFile is the file in a session directory the runner writes the absolute path of a sandbox-backed session's primary workspace (the directory made under os.TempDir() in place of the working directory, when the working directory itself cannot be the primary workspace) to, once that directory exists. It is removed with the workspace when the session ends, so a session directory holding one is a session whose primary workspace may still exist on disk. CleanSandboxes and CleanSandboxDirs read it and remove that directory tree, subject to the workspace path rule (see validWorkspacePath).

View Source
const ServerPIDFile = "mcp-server-pid"

ServerPIDFile is the file in a session directory the runner writes the pid of the optional caller-supplied MCP server it started on the host for a container-backed session to. The server runs in a process group of its own, outside the session's and outside the scheduler's, so nothing else reaches it: a crash that takes the scheduler down without running its deferred cleanup leaves the server running, holding its port and serving the factory's tools, and this file is the only record of it.

It is a file of its own rather than PIDFile, which names the session's main command: a container session leaves both.

View Source
const SessionMarker = "--name agent-"

SessionMarker is the argv fragment that identifies a claude session of the default runner: the --name every one is started with.

Variables

View Source
var AgentExecutables = []string{"claude", "codex", "opencode", "pi"}

AgentExecutables are the basenames of the agent commands a session runs. A process running one of them is an agent session of some factory, which is what tells a pid file whose session the ps scan cannot match (an opencode session carries no marker, and an agent that rewrites its argv carries nothing at all) from a pid reused by an unrelated process.

The runner's agent list is this one (agent.Agents), so an agent added there is recognized here.

View Source
var Engine = "docker"

Engine is the container engine command asked which containers are running and told to remove one. It is a variable so a test can answer for a machine that has no container engine; nothing but a test changes it.

Functions

func Alive

func Alive(pid int) bool

Alive reports whether a process exists.

func CleanSandboxDirs

func CleanSandboxDirs(ctx context.Context, sbxBin string, dirs []string) error

CleanSandboxDirs is CleanSandboxes for a caller whose sessions do not all live under one sessions directory: every directory given is attempted in turn, through cleanSandboxDir, and the per-directory errors are joined (errors.Join), each naming the session directory and the sandbox or record it concerns, so that one directory's failure never keeps the others from being cleaned up.

func CleanSandboxes

func CleanSandboxes(ctx context.Context, sbxBin, sessionsDir string) error

CleanSandboxes removes the orphaned Docker Sandbox, and the orphaned primary workspace, of every session directory held in sessionsDir, by collecting its directory entries and asking CleanSandboxDirs about them, the way FromPIDFiles asks FromPIDFile about every entry in turn. sbxBin is the sbx binary to run — a caller's Runner.SbxBin, or agent.SandboxCLI by default — because this package cannot import agent. It must run at startup, before any session starts, so that every sandbox-name or primary-workspace record it finds belongs to a session an earlier process died without closing, never one still in use.

func CodexMarker

func CodexMarker(prefix string) string

CodexMarker returns the session-directory override for a caller's namespace.

func ContainerID

func ContainerID(dir string) string

ContainerID returns the id of the container a session directory records, empty when the session does not run in one.

func Kill

func Kill(p Proc, grace time.Duration) error

Kill stops a session: its container, when it runs in one, its process and process group, and the caller-supplied MCP server on the host when it left one — SIGTERM, then SIGKILL after grace if it is still alive. The container is removed first, because it outlives the engine client that started it and the agent is inside it; the server goes last, so the tools it serves stay answerable until the session using them is gone. A session found through its container alone has no process left to signal.

func RemoveContainerID

func RemoveContainerID(dir string)

RemoveContainerID deletes a session's container id file.

func RemovePID

func RemovePID(sessionDir string)

RemovePID deletes a session's pid file.

func RemoveSandboxName

func RemoveSandboxName(dir string)

RemoveSandboxName deletes a session's sandbox name file.

func RemoveServerPID

func RemoveServerPID(dir string)

RemoveServerPID deletes a session's server pid file.

func SandboxName

func SandboxName(dir string) string

SandboxName returns the name of the Docker Sandbox a session directory records, empty when the session does not run in one.

func SandboxWorkspace

func SandboxWorkspace(dir string) string

SandboxWorkspace returns the absolute path of the primary workspace a session directory records, empty when the session recorded none.

func ServerPID

func ServerPID(dir string) int

ServerPID returns the pid of the host-side MCP server a session directory records, 0 when the session runs none.

func WritePID

func WritePID(sessionDir string, pid int) error

WritePID records the pid of a running session.

func WriteSandboxName

func WriteSandboxName(dir, name string) error

WriteSandboxName records the name of a session's Docker Sandbox.

func WriteServerPID

func WriteServerPID(dir string, pid int) error

WriteServerPID records the pid of the host-side MCP server of a container-backed session.

Types

type Finder

type Finder struct {
	// Markers identify the caller's sessions in the process table and its
	// containers by label. Nil is the default set; a Markers given here is
	// used as it stands, so an empty one matches no process and no
	// container, exactly as passing Markers{} to the package functions does.
	Markers *Markers
	// ReadOnly keeps discovery from writing under the sessions directory:
	// nothing stale is deleted, however plainly stale it is.
	ReadOnly bool
}

Finder is session discovery with the options a caller needs. The package functions Find, FromPIDFile, FromPIDFiles and FromContainers are a Finder carrying the markers they are given and nothing else.

Discovery ordinarily writes: a pid, container-id or server-pid file naming a process, container or server that is gone is deleted as it is read, which is how cleanup keeps the sessions directory honest. A caller that must only look — an inspection command, a dry run — builds a Finder with ReadOnly set and gets the same []Proc with every file left in place.

func (Finder) Find

func (f Finder) Find(ctx context.Context, sessionsDir string) ([]Proc, error)

Find is Find with the finder's options: a read-only finder reports the same sessions and deletes nothing.

func (Finder) FromContainers

func (f Finder) FromContainers(ctx context.Context, sessionsDir string) ([]Proc, error)

FromContainers is FromContainers with the finder's options: a read-only finder deletes no stale container id file.

func (Finder) FromPIDFile

func (f Finder) FromPIDFile(dir string, scan *Scan) (Proc, bool)

FromPIDFile is FromPIDFile with the finder's options.

func (Finder) FromPIDFiles

func (f Finder) FromPIDFiles(sessionsDir string, scan *Scan) ([]Proc, error)

FromPIDFiles is FromPIDFiles with the finder's options: a read-only finder deletes no pid file.

type Markers

type Markers struct {
	Session, Codex, Container string
	// LegacyCodex optionally recognizes codex sessions that carry an older runner's marker.
	LegacyCodex string
}

Markers describes the caller's session command markers and container label.

type Proc

type Proc struct {
	PID     int
	PGID    int
	Command string
	// Source is "pidfile", "ps" or "container".
	Source string
	// SessionDir is set for sessions found through a pid file or through
	// their container, whose label carries it.
	SessionDir string
	// Container is the id of the container a session runs in, for a
	// session in the container sandbox. Stopping such a session means
	// removing the container: the agent runs inside it and outlives the
	// engine client PID names.
	Container string
	// Server is the pid of the caller-supplied MCP server running on the host for
	// a session in the container sandbox, which has no caller binary inside.
	// It is a process group of its own, so stopping the session means
	// stopping it too (see ServerPIDFile).
	Server int
}

Proc is a process that looks like an agent session.

func Find

func Find(ctx context.Context, sessionsDir string, markers ...Markers) ([]Proc, error)

Find merges pid-file and ps results, de-duplicated by pid, for the factory whose sessions live in sessionsDir. Pid files are cross-checked against the process table when it is available.

func FromContainers

func FromContainers(ctx context.Context, sessionsDir string, markers ...Markers) ([]Proc, error)

FromContainers returns the container-backed sessions of the factory whose sessions live in sessionsDir, by asking the engine which of its running containers carry ContainerLabel with a session directory of this factory. The engine is the authority the process table is for a session on the host: a session directory recording a container the engine does not list is a session that has ended, and its stale id file is deleted the way FromPIDFile deletes the pid file of a process that is gone.

It fails when there is no engine to ask, which is also the answer to "are any container sessions running": none that can be found or stopped.

func FromPIDFile

func FromPIDFile(dir string, scan *Scan) (Proc, bool)

FromPIDFile returns the live session recorded in one session directory: the process its pid file names, the container it runs in and the host-side MCP server it left, the last two for a container-backed session only.

It reports false when the directory records none of the three, so a session directory whose main process is gone but whose server is still running is still reported — with PID 0, because there is no process to signal and the server is stopped in its own right. That is what a crash leaves behind.

func FromPIDFiles

func FromPIDFiles(sessionsDir string, scan *Scan) ([]Proc, error)

FromPIDFiles returns live sessions recorded under sessionsDir and deletes pid files of processes that no longer exist, by asking FromPIDFile about every session directory in turn.

func FromPS

func FromPS(ctx context.Context, sessionsDir string, markers ...Markers) ([]Proc, error)

FromPS scans the process table for agent sessions: processes whose executable is one of AgentExecutables, whose arguments carry a session marker and whose command line references sessionsDir, the sessions directory of this factory's state directory.

type Scan

type Scan struct {
	Sessions map[int]Proc
	Commands map[int]string
}

Scan is what one reading of the process table said: the sessions of this factory found in it, keyed by pid, and the command line of every process it listed. The commands answer what the sessions cannot: whether the process a pid file names is an agent the scan's markers do not match.

A nil *Scan means no reading of the process table was available, and a pid file is then trusted on its own.

Jump to

Keyboard shortcuts

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