Documentation
¶
Overview ¶
Package repo is the orchestration layer: it ties dag (metadata) + btrfs (snapshots) + image (rootfs) + sandbox (execution) together and exposes the high-level operations init/exec/spawn/commit/checkout/log/gc.
A long-lived agent (see `agentenv serve`) keeps one *Repo for its whole session. Background processes started with Spawn are tracked in memory; on Checkout they are killed (the agent process itself, living in the control-root, is never touched). This realizes "agent survives, other processes reset".
On-disk layout (AGENTENV_ROOT, default /agentfs, must live on btrfs):
<root>/ nodes/<id>/ one read-only snapshot per immutable node work/current/ the live writable inner-env subvolume (commands run here) logs/<id>.log output of background (spawned) processes meta.json commit-DAG metadata
Index ¶
- type CandidateResult
- type Capturer
- type Change
- type Lock
- type Proc
- type Repo
- func (r *Repo) Backend() *backend.Backend
- func (r *Repo) Checkout(ref string) error
- func (r *Repo) CheckoutCount() int
- func (r *Repo) Commit(message, command string) (*dag.Node, error)
- func (r *Repo) Delete(ref string) error
- func (r *Repo) Diff(aRef, bRef string) ([]Change, error)
- func (r *Repo) Exec(args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error)
- func (r *Repo) GC() ([]string, error)
- func (r *Repo) Head() string
- func (r *Repo) InitFrom(src string) (*dag.Node, error)
- func (r *Repo) InitTarball(src string) (*dag.Node, error)
- func (r *Repo) Kill(pid int) error
- func (r *Repo) Leaves() []*dag.Node
- func (r *Repo) Nodes() []*dag.Node
- func (r *Repo) ProcAlive(pid int) bool
- func (r *Repo) Procs() []*Proc
- func (r *Repo) ResolveRef(ref string) string
- func (r *Repo) Retain(keepRecent int) []string
- func (r *Repo) RunInteractive(args []string) (int, error)
- func (r *Repo) SetPreserveProcs(v bool)
- func (r *Repo) SetTag(name, ref string) error
- func (r *Repo) Shell(args []string) (int, error)
- func (r *Repo) Show(ref string) ([]Change, string, error)
- func (r *Repo) Spawn(args []string) (*Proc, error)
- func (r *Repo) StartCapturer() *Capturer
- func (r *Repo) Status() Status
- func (r *Repo) Tags() map[string]string
- func (r *Repo) Tournament(baseRef, test string, candidates []struct{ ... }, keep bool) (*TournamentResult, error)
- func (r *Repo) Tree() string
- type Status
- type TournamentResult
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type CandidateResult ¶
type CandidateResult struct {
Name string // human label ("+", "B", "v1.2", ...)
Cmd string // shell command run inside the env to produce this branch
Node string // resulting branch tip (empty on build failure)
TestExit int // exit code of the test command on this branch (-1 if not run)
Err string // non-empty if the branch failed to build or test
}
CandidateResult is what one branch of a tournament produced.
type Capturer ¶
type Capturer struct {
// contains filtered or unexported fields
}
Capturer auto-snapshots the inner-env whenever it changes and settles. Change detection is channel-agnostic and, where possible, event-driven:
- An inotify Watcher signals on any change (shell command OR direct file edit), so the idle path does no filesystem walking.
- A periodic Token() check is kept as a backstop (and as the only mechanism if the watcher can't be created).
This removes the need for the agent to ever call "commit" — the environment versions itself like a DVR.
func (*Capturer) Flush ¶
Flush snapshots immediately if the env changed since the last node (used right after a shell command so the node lands promptly with the command's label).
func (*Capturer) SetLabel ¶
SetLabel hints the label for the next snapshot (e.g. the command being run).
func (*Capturer) SetOnSnapshot ¶
SetOnSnapshot registers a callback invoked after each auto-snapshot (UI feedback).
type Lock ¶
type Lock struct {
// contains filtered or unexported fields
}
Lock holds an exclusive flock on the repo root so two mutating sessions cannot corrupt meta.json. Read-only commands skip it.
Why a struct instead of a `func() / *os.File` pair: the *os.File must stay reachable until release, but a `func()` closure that captures it doesn't keep it from being GC'd if the GC's escape analysis decides the closure itself is short-lived (it can be — callers `defer release()` then drop the variable). A returned struct is naturally kept by the caller's `defer lock.Release()`.
func AcquireLock ¶
AcquireLock takes the exclusive non-blocking flock and returns a Lock the caller must Release(). The kernel also drops the flock on process exit, but callers SHOULD route process termination through main (not os.Exit from random call sites) so deferred Release fires and so any in-flight save can finish.
type Proc ¶
type Proc struct {
PID int
Args []string
Started time.Time
Log string
// contains filtered or unexported fields
}
Proc is a tracked background process running in the inner-env.
type Repo ¶
type Repo struct {
// contains filtered or unexported fields
}
func (*Repo) Checkout ¶
Checkout rolls back / switches to any node (also accepts tag names or unique ID prefixes via ResolveRef). It kills the inner-env processes, rebuilds the working volume from that node, and points HEAD there. A later commit then forks under it, forming a tree. The agent process is untouched.
func (*Repo) CheckoutCount ¶
CheckoutCount returns how many checkouts (rollbacks) have happened — used by the supervisor to tell a rollback-kill from a normal agent exit.
func (*Repo) Delete ¶ added in v0.2.0
Delete removes a node from the DAG and deletes its on-disk snapshot. Its children are re-parented to its parent so the tree stays connected (deleting a middle node keeps its descendants). Refuses to delete the current HEAD (it would orphan the live working tree — checkout elsewhere first) or the last remaining node. Deleting a node's snapshot is safe even if children hardlink-share its files: the shared inodes stay alive as long as a child still references them (same reason GC is safe).
func (*Repo) Diff ¶
Diff lists the paths that differ between node aID and node bID (either may be "" to mean an empty tree). Accepts tag names or unique ID prefixes; ref resolution and DAG access happen UNDER the same opMu so a concurrent commit or retention pass can't change the resolution between resolve and use.
func (*Repo) Exec ¶
Exec runs a real command in the current working volume (foreground) and waits. It returns the command's exit code and an error. A command that runs to completion with a non-zero status is NOT an agentenv error: it returns (code, nil). A non-nil error means agentenv itself failed (could not set up the volume, start the process, etc.).
func (*Repo) GC ¶
GC deletes node subvolumes on disk that are not referenced by the DAG (orphans), e.g. nodes dropped by retention. This is where sparsified snapshots are actually freed from disk.
func (*Repo) InitFrom ¶
InitFrom builds the root node by seeding the rootfs from an existing directory (e.g. "/" — the current container filesystem — or a workspace dir) instead of downloading a base image. This makes the managed environment BE the agent's real environment, so the agent can run inside it (system installs + files all captured) without changing its code — only its launch command.
func (*Repo) InitTarball ¶
InitTarball builds the root node by extracting a .tar / .tar.gz at src (filesystem path or http(s) URL) into the working rootfs. Use this for demo rootfs tarballs and air-gapped setups; for production, use InitFrom to seed from the agent's own running container.
func (*Repo) Kill ¶
Kill terminates one tracked background process (and its inner-env PID namespace).
func (*Repo) Leaves ¶
Leaves returns the branch tips (nodes with no children), oldest first. Each tip is a distinct explored end-state — the unit of branch exploration.
func (*Repo) ResolveRef ¶
ResolveRef turns a tag name OR a node ID (or unique ID prefix) into a node ID. It returns "" if nothing matches. Used by Checkout, Show, Diff, and the API so the agent/operator can refer to nodes by short, memorable names. ResolveRef resolves a ref (tag, full ID, or unique ID prefix) to a node ID, taking opMu around the lookup. Returns "" if not found / ambiguous / unsafe.
IMPORTANT: ResolveRef takes the lock and releases it before returning. Any caller that needs to act on the resolved ID atomically (Checkout / Show / Diff / SetTag — they all read the DAG further after resolving) MUST use resolveRefLocked while already holding opMu, otherwise a concurrent op can invalidate the resolution between the resolve and the act on it (TOCTOU).
func (*Repo) Retain ¶
Retain sparsifies the DAG: it keeps all "recent" and structural nodes and thins older interior nodes on an exponential (DVR ring) curve, so the node count stays ~ keepRecent + log2(total). Dropped nodes are removed from the DAG (their children are re-linked to the nearest surviving ancestor); their on-disk snapshots become orphans, reclaimed by GC. Returns the dropped node IDs.
Always kept:
- the root and HEAD
- branch points (>=2 children) and leaves (branch tips) — pruning these would lose reachable states
- the newest keepRecent non-structural nodes
Then, among older non-structural nodes ordered newest->oldest by rank i, only ranks where i is 2^k-1 (0,1,3,7,15,31,...) are kept.
func (*Repo) RunInteractive ¶ added in v0.2.0
RunInteractive runs an interactive agent on a PTY (supervise's TTY mode) and registers it as the foreground process so a concurrent Checkout() can kill it. Returns the agent's exit code. The supervisor calls this in a loop: after a rollback kills the agent, CheckoutCount increases and the supervisor re-invokes RunInteractive to relaunch from the restored environment.
func (*Repo) SetPreserveProcs ¶ added in v0.2.0
SetPreserveProcs enables self-rollback mode: Checkout reverts the working rootfs in place WITHOUT killing running processes, so the agent that triggered the rollback survives and observes the reverted environment.
func (*Repo) SetTag ¶
SetTag assigns name to a node ID (or another ref). Empty id removes the tag. Tag names cannot collide with node IDs (12-hex chars) — guarded here.
func (*Repo) Shell ¶
Shell runs an interactive shell inside the current working volume on a PTY, returning its exit code. File changes made in the shell are auto-captured by the Capturer running alongside it — no per-command wrapping or commit needed.
func (*Repo) Show ¶
Show returns what a node changed relative to its parent (like `git show --stat`), along with the parent ID ("" if it is the root). Accepts refs. Same lock discipline as Diff: resolve + read happen atomically.
func (*Repo) Spawn ¶
Spawn starts a background process in the current working volume, tracks it, and returns its handle. Output is written to logs/<id>.log. A reaper goroutine removes it from the table when it exits.
func (*Repo) StartCapturer ¶
StartCapturer launches the background capture loop and records it on the repo so Checkout can coordinate (pause) with it.
func (*Repo) Status ¶
Status returns a runtime snapshot. Cheap: takes the lock briefly and stats the root tree once.
func (*Repo) Tournament ¶
func (r *Repo) Tournament(baseRef, test string, candidates []struct{ Name, Cmd string }, keep bool) (*TournamentResult, error)
Tournament forks the environment N times from base, runs each candidate concurrently in its own isolated workspace, then runs test concurrently in each, and picks the first candidate (by input order, for determinism) whose test exits 0. The main work/current is NOT touched during the run; only the final HEAD reposition (always done at the end) affects it.
- keep==true && there is a winner → HEAD lands on the winner's node.
- otherwise → HEAD lands on the base.
Build and test phases are parallel; the only serialization point is a brief opMu hold per branch around assigning a node ID and adding to the DAG. The backend workspace primitives are concurrent-safe (each touches a distinct path), and the sandbox runner does too (each invocation forks its own child in its own namespaces).
type Status ¶
type Status struct {
Backend string
Root string
Head string
NodeCount int
LeafCount int
ProcCount int
DiskBytes int64 // bytes on disk under root (real, hardlink-shared files counted once)
PollMs int
DebounceMs int
Ignore []string // rootfs-relative paths currently excluded from snapshots
}
Status is a one-shot snapshot of the repo's runtime state (for `agentenv status`).
type TournamentResult ¶
type TournamentResult struct {
Base string // resolved base node id
Candidates []CandidateResult // one per candidate, in input order
Winner string // candidate name whose test exit==0 (first such by input order)
WinnerNode string // the winning node id, "" if none
}
TournamentResult is the outcome of one tournament call.