Documentation
¶
Overview ¶
Package process defines the Tools-owned long-running-command supervision domain: process identity, lifecycle state, the stable error taxonomy, and quota configuration (spec "docs/specs/long-running-command-supervision.md", sections "Identity and authorization", "State machine", "Stable errors", and "Quotas and retention"). This package has no dependency on github.com/looprig/harness; later tasks wire this domain to Harness's AsyncProcessRunner/PreparedProcess contracts.
Index ¶
- Constants
- Variables
- func NewSupervisorResource(dir string) (tool.SessionResource, error)
- type AccessMode
- type Artifact
- type Base64Result
- type Buffer
- type Code
- type CollisionError
- type CommandMetadata
- type Config
- type Error
- type GenerateError
- type Handle
- type HandleExists
- type Identity
- type ImmutableIdentityChangedError
- type Lease
- type LifecycleEventIDChangedError
- type LifecycleEventIDs
- type Manifest
- type ManifestStore
- type NonMonotonicUpdateError
- type Origin
- type Owner
- type ProcessInputTool
- func (t *ProcessInputTool) Info(context.Context) (*tool.ToolInfo, error)
- func (t *ProcessInputTool) InvokableRun(ctx context.Context, _ string) (*tool.ToolResult, error)
- func (t *ProcessInputTool) PrepareCall(_ context.Context, _ uuid.UUID, argsJSON string) (tool.Request, tool.PreparedArtifact, error)
- type ProcessOutputTool
- func (t *ProcessOutputTool) Info(context.Context) (*tool.ToolInfo, error)
- func (t *ProcessOutputTool) InvokableRun(ctx context.Context, _ string) (*tool.ToolResult, error)
- func (t *ProcessOutputTool) PrepareCall(_ context.Context, _ uuid.UUID, argsJSON string) (tool.Request, tool.PreparedArtifact, error)
- type ProcessStopTool
- type Reader
- type RestoreError
- type RestoreReport
- type Result
- type SafeTextResult
- type Spool
- type SpoolCursors
- type State
- type StorageCeiling
- type Supervisor
- func (s *Supervisor) Restore(ctx context.Context) (RestoreReport, error)
- func (s *Supervisor) Shutdown(ctx context.Context) error
- func (s *Supervisor) Start(ctx context.Context, owner Owner, origin Origin, prepared tool.PreparedProcess, ...) (Handle, error)
- func (s *Supervisor) Wait(ctx context.Context, owner Owner, kind WaitKind, targets []WaitTarget) ([]WaitStatus, error)
- type SupervisorResource
- type TerminalResultChangedError
- type TransitionError
- type WaitKind
- type WaitStatus
- type WaitTarget
- type YieldSettings
Constants ¶
const ( DefaultMaxRunningProcessesPerLoop = 8 DefaultMaxRunningProcessesPerSession = 32 DefaultMaxRetainedCompletedProcessesPerSession = 100 // DefaultMaxProcessInMemoryBytes is the spec's 1 MiB per-process // in-memory rolling window default. DefaultMaxProcessInMemoryBytes int64 = 1 << 20 // 1 MiB (spec) // DefaultMaxAggregateInMemoryBytes derives from the per-process default // times the per-session concurrency default. DefaultMaxAggregateInMemoryBytes int64 = int64(DefaultMaxRunningProcessesPerSession) * DefaultMaxProcessInMemoryBytes // DefaultMaxProcessSpoolBytes is the spec's 64 MiB per-process disk // spool default. DefaultMaxProcessSpoolBytes int64 = 64 << 20 // 64 MiB (spec) // DefaultMaxAggregateSpoolBytes derives from the per-process default // times the per-session concurrency default. DefaultMaxAggregateSpoolBytes int64 = int64(DefaultMaxRunningProcessesPerSession) * DefaultMaxProcessSpoolBytes // DefaultMaxInlineResultBytes is the spec's 32 KiB inline model result // default. DefaultMaxInlineResultBytes int64 = 32 << 10 // 32 KiB (spec) DefaultMaxPendingWaiters int = 64 DefaultMaxPendingInputBytes int64 = 1 << 20 // 1 MiB // DefaultGracefulShutdownPeriod is the spec's 5 second graceful // shutdown default. DefaultGracefulShutdownPeriod = 5 * time.Second // (spec) )
Documented zero-value defaults. Values annotated "(spec)" are the exact numbers from the spec's "Output capture and storage" defaults table. The remaining defaults are conservative operational values chosen for this task, not spec-mandated, and any Config value may override them; the per-process/aggregate pairs are deliberately derived from each other so the defaults are self-consistent (a per-process default never exceeds its aggregate default) without hand-tuning a second number.
const ArtifactEncodingBase64 = "base64"
ArtifactEncodingBase64 is the only Encoding value Artifact currently carries: raw bytes retrieved through a base64 ProcessOutput read (spec "ProcessOutput API": `"artifact": {"id": "opaque", "encoding": "base64"}`).
const HandleEntropyBytes = 16
HandleEntropyBytes is the raw random byte length backing a Handle: 16 bytes = 128 bits, the spec's documented minimum ("Output capture and storage": "Process handle entropy | at least 128 bits").
const SupervisorResourceKey = "github.com/looprig/tools/process.supervisor"
SupervisorResourceKey names the ONE shared process.Supervisor session resource every supervised Bash call (bash/supervised.go's runSupervised) and every ProcessOutput/ProcessInput/ProcessStop call (this module's root definitions.go) obtain through tool.SessionResourceRegistry.GetOrCreate: "any of the four definitions may win get-or-create" requires all of them to key on this exact same string. Both consumer packages import this one constant rather than each keeping their own private copy, so there is a single authoritative source for the key.
Variables ¶
var ErrSpoolClosed = errors.New("process: spool is closed")
ErrSpoolClosed reports that an operation was attempted against a Spool after Close or Remove. It is a plain sentinel, not a *Error, because it signals local misuse of the Spool value's lifetime rather than a model-facing supervision failure.
Functions ¶
func NewSupervisorResource ¶
func NewSupervisorResource(dir string) (tool.SessionResource, error)
NewSupervisorResource is the tool.SessionResourceRegistry.GetOrCreate factory for SupervisorResourceKey: it is runner-free (constructs no tool.AsyncProcessRunner and calls neither PrepareProcess nor Start), so any of the four process-backed definitions may win the get-or-create race for a session's shared supervisor. dir is the private per-session storage directory the registry reserves for this key.
Types ¶
type AccessMode ¶
type AccessMode string
AccessMode is the manifest's sanitized record of the effective workspace access a process was granted at spawn (spec "Workspace coordination", "Lease compatibility": read-only, scoped write, broad/workspace write). Tools does not import Harness's WorkspaceAccess type; this is a narrow, stable local mirror recorded for manifest/audit purposes only.
const ( AccessReadOnly AccessMode = "read_only" AccessScopedWrite AccessMode = "scoped_write" AccessBroadWrite AccessMode = "broad_write" )
The closed set of recorded access modes.
func (AccessMode) Valid ¶
func (a AccessMode) Valid() bool
Valid reports whether a belongs to the closed AccessMode domain.
type Artifact ¶
Artifact is an opaque, path-free reference to a bounded window of a process's raw output (spec "Output capture and storage": "raw bytes remain only in the bounded spool and are exposed only through an opaque artifact descriptor plus owner-authorized ProcessOutput base64 reads"; "No filesystem path to a spool or manifest is returned to a model"). Every field is independently already safe to hand to a model: ProcessID is a Handle, which by construction carries no filesystem path, owner identifier, or OS PID (see identity.go's Handle doc); StartCursor and EndCursor are plain integers. There is no path, file descriptor, or other host detail anywhere in this type. A caller retrieves the bytes an Artifact describes with a base64 ProcessOutput read against ProcessID at StartCursor (spec: "callers retrieve its bytes with ProcessOutput and the original process handle, cursor, and base64 encoding").
func NewArtifact ¶
NewArtifact builds the opaque descriptor for the raw byte range [startCursor, endCursor) of handle's output.
type Base64Result ¶
type Base64Result struct {
// Data is the raw bytes Read returned, base64-encoded byte for byte
// with no normalization applied.
Data string
// StartCursor is the cursor Read was called with (before any
// gap adjustment).
StartCursor int64
// NextCursor is the exclusive cursor immediately after the bytes Read
// actually returned.
NextCursor int64
// Gap reports whether StartCursor fell before the earliest retained
// byte, exactly as Reader.Read documents.
Gap bool
}
Base64Result is the base64 render outcome for one bounded, cursor-addressed read: the exact raw bytes from r, unmodified and unnormalized, base64-encoded (spec "ProcessOutput API": "`base64` reads the same owner-authorized raw spool bytes without exposing a host path").
func RenderBase64 ¶
func RenderBase64(r Reader, cursor int64, maxBytes int) (Base64Result, error)
RenderBase64 reads up to maxBytes of retained output from r starting at cursor and returns it base64-encoded, byte for byte, with no normalization applied. It reuses exactly the same Reader/cursor/maxBytes plumbing as RenderSafeText -- "the same owner check and byte limits as safe text" from the task doc means both render modes share this one read path; only RenderSafeText additionally normalizes and caps. RenderBase64 performs no owner authorization itself (see Reader's doc); that is Task 8's supervisor's responsibility before either render function is called.
type Buffer ¶
type Buffer struct {
// contains filtered or unexported fields
}
Buffer is the in-memory rolling window over one process's combined stdout+stderr byte stream (spec "Output capture and storage": "The in-memory window is optimized for recent polling. The spool is the bounded source of truth for completed output and cursor recovery"). Buffer and Spool (spool.go) share one cursor addressing scheme -- raw, monotonically increasing byte offsets into a single combined append-ordered stream, never rune or codepoint indexes -- but are two independent stores: Buffer is a fixed-capacity ring kept entirely in memory for cheap recent-output polling. It never persists to disk and never reads from or delegates to a Spool; a Buffer alone is not the source of truth for anything older than its own capacity.
A Buffer is safe for concurrent use by multiple goroutines.
func NewBuffer ¶
NewBuffer returns a Buffer with the given capacity in bytes. A non-positive capacity defaults to DefaultMaxProcessInMemoryBytes (config.go), mirroring OpenSpool's non-positive-ceiling default.
func (*Buffer) Append ¶
Append durably (in the in-memory sense -- see the type doc for what "durable" does not mean here) adds data to the end of the combined output stream and returns the resulting global cursor (equal to TotalBytes after the append). Append order determines the global cursor, exactly as for Spool.Append.
Once the retained window would exceed capacity, Append overwrites the oldest retained bytes in place (true ring wraparound: no reallocation, no growth beyond capacity) rather than blocking or failing; TotalBytes keeps counting every byte ever appended, independent of what remains retained. A single Append whose data is itself longer than capacity is handled the same way an equivalent sequence of smaller Appends would be: only the last capacity bytes of data end up retained.
func (*Buffer) Capacity ¶
Capacity reports the maximum number of retained bytes b was constructed with.
func (*Buffer) Read ¶
func (b *Buffer) Read(cursor int64, maxBytes int) (data []byte, nextCursor int64, gap bool, err error)
Read returns up to maxBytes of retained output starting at cursor, the exclusive cursor immediately after the returned data (nextCursor), and whether cursor fell before the earliest retained byte (gap). This mirrors Spool.Read's exact signature and semantics: when gap is true, the returned data begins at the earliest retained byte rather than at cursor. A non-positive maxBytes returns every retained byte from the (possibly gap-adjusted) start.
A cursor beyond TotalBytes reports CodeCursorAhead. A negative cursor reports CodeInvalidArguments.
func (*Buffer) RetainedFrom ¶
RetainedFrom reports the cursor of the earliest byte currently retained in memory.
func (*Buffer) TotalBytes ¶
TotalBytes reports the monotonically increasing count of every byte ever appended, independent of what remains retained.
type Code ¶
type Code string
Code is a stable, model-facing process-supervision failure classification (spec "Stable errors"). The set is closed; render only Code — never Cause — at an untrusted or model-facing boundary, so host paths, OS PIDs, and cross-owner details never leak.
const ( CodeInvalidArguments Code = "invalid_arguments" CodeInvalidSettings Code = "invalid_settings" CodeProcessQuotaExceeded Code = "process_quota_exceeded" CodeOutputQuotaExceeded Code = "output_quota_exceeded" CodeProcessNotificationsUnsupported Code = "process_notifications_unsupported" CodeSpawnFailed Code = "spawn_failed" CodeProcessSetupFailed Code = "process_setup_failed" CodeNotFound Code = "not_found" CodeStdinClosed Code = "stdin_closed" CodeInputBackpressure Code = "input_backpressure" CodeCursorGap Code = "cursor_gap" CodeCursorAhead Code = "cursor_ahead" CodeTimedOut Code = "timed_out" CodeInterrupted Code = "interrupted" CodeTerminated Code = "terminated" CodeKilled Code = "killed" CodeSupervisorShuttingDown Code = "supervisor_shutting_down" CodeManifestCorrupt Code = "manifest_corrupt" CodeSpoolCorrupt Code = "spool_corrupt" CodeLostOnRestore Code = "lost_on_restore" CodeTeardownFailed Code = "teardown_failed" )
The closed stable error taxonomy (spec "Stable errors").
type CollisionError ¶
type CollisionError struct{ Attempts int }
CollisionError reports that handle generation exhausted maxGenerateAttempts candidates that all reported as already in use by the supplied HandleExists check.
func (*CollisionError) Error ¶
func (e *CollisionError) Error() string
type CommandMetadata ¶
type CommandMetadata struct {
// Command is the shell command line as supplied to Bash.
Command string
// WorkDir is the resolved working directory, when the call requested
// one.
WorkDir string
}
CommandMetadata is the sanitized, non-secret description of the command a manifest records (spec "Manifests and durability": "sanitized command metadata"). It never carries environment, stdin content, or captured output.
type Config ¶
type Config struct {
// MaxRunningProcessesPerLoop bounds concurrently running processes
// belonging to one loop.
MaxRunningProcessesPerLoop int
// MaxRunningProcessesPerSession bounds concurrently running processes
// across an entire session (every loop in it).
MaxRunningProcessesPerSession int
// MaxRetainedCompletedProcessesPerSession bounds how many completed
// process manifests a session keeps queryable before least-recently-used
// eviction (spec "Quotas and retention": "Completed metadata is evicted
// by least-recently-used order only after the retention limit is
// reached").
MaxRetainedCompletedProcessesPerSession int
// MaxProcessInMemoryBytes bounds one process's in-memory rolling output
// window (spec "Output capture and storage" default: 1 MiB per
// process).
MaxProcessInMemoryBytes int64
// MaxAggregateInMemoryBytes bounds the sum of every process's
// in-memory rolling window across a session.
MaxAggregateInMemoryBytes int64
// MaxProcessSpoolBytes bounds one process's disk spool retention window
// (spec default: 64 MiB per process). This is a bounded retention
// window, not a hard cap on total process output (spec "Output capture
// and storage": "The spool is a bounded retention window, not a hard
// cap on the process").
MaxProcessSpoolBytes int64
// MaxAggregateSpoolBytes bounds the sum of every process's disk spool
// across a session.
MaxAggregateSpoolBytes int64
// MaxInlineResultBytes bounds the output returned inline in a
// model-facing result before a caller must page through ProcessOutput
// (spec default: 32 KiB).
MaxInlineResultBytes int64
// MaxPendingWaiters bounds the outstanding ProcessOutput wait: any|all
// waiters a session admits concurrently.
MaxPendingWaiters int
// MaxPendingInputBytes bounds unconsumed ProcessInput data queued for
// one process, so a call can never block indefinitely behind a process
// that does not read its stdin (spec "ProcessInput API": "Writes are
// serialized per process and bounded").
MaxPendingInputBytes int64
// GracefulShutdownPeriod is how long supervisor shutdown and
// ProcessStop's terminate mode wait before escalating to kill (spec
// default: 5 seconds).
GracefulShutdownPeriod time.Duration
}
Config is the process-supervisor quota and retention surface described by the spec's "Quotas and retention" section, plus the numeric defaults from its "Output capture and storage" defaults table. The zero Config is valid: Normalize fills every zero field with its documented default.
func (Config) Normalize ¶
Normalize validates c and, if valid, returns it with every zero field replaced by its documented default (withDefaults). The zero Config normalizes to every documented default with no error. A negative field, or an explicit value that exceeds an explicit counterpart, returns the zero Config and a *Error with CodeInvalidSettings.
Validation runs on c before defaulting, not on the defaulted result: Validate's cross-field checks are documented to reject only explicit inconsistencies between two nonzero fields, and that "zero means unset" contract only holds against the caller's original input. Two independent per-field defaults are proven self-consistent with each other (TestDefaultsAreSelfConsistent), but a default chosen for one field is not guaranteed to stay under a smaller *explicit* value the caller set for its counterpart (e.g. an explicit, low MaxRunningProcessesPerSession with MaxRunningProcessesPerLoop left at zero) -- validating post-default would reject that legitimate partial override merely because defaulting filled in the other side.
func (Config) Validate ¶
Validate rejects a negative limit and any explicit per-process limit that exceeds its explicit aggregate (or broader-scope) counterpart. A zero field is untouched by Validate — it means "unset, use the documented default" (see Normalize) — so only strictly negative values and explicit inconsistencies between two nonzero fields are rejected.
type Error ¶
Error reports one classified process-supervision failure. Cause carries implementation detail for programmatic inspection and trusted logs; Code is the stable, model-safe classification (spec "Stable errors": "Errors must support errors.Is or typed inspection and render to stable model-facing codes without exposing host paths, OS PIDs, or cross-owner details").
type GenerateError ¶
type GenerateError struct{ Err error }
GenerateError reports a failure to read randomness while minting a process Handle. It wraps the underlying source error so callers can errors.As to a *GenerateError and errors.Unwrap (or read .Err) to inspect the cause.
func (*GenerateError) Error ¶
func (e *GenerateError) Error() string
func (*GenerateError) Unwrap ¶
func (e *GenerateError) Unwrap() error
type Handle ¶
type Handle string
Handle is a process's opaque, URL-safe capability identifier. It is cryptographically random with at least HandleEntropyBytes of entropy and carries no owner, filesystem path, timestamp, or OS process identifier: nothing about its bytes can be inspected to recover any of those (spec "Identity and authorization": "ProcessID is an opaque, cryptographically random handle with at least 128 bits of entropy. It must not encode an OS PID, filesystem path, owner identifier, or creation timestamp").
func NewHandle ¶
func NewHandle(exists HandleExists) (Handle, error)
NewHandle mints a new Handle sourced from crypto/rand, retrying against exists (which may be nil to skip the check) up to maxGenerateAttempts times before returning a *CollisionError.
type HandleExists ¶
HandleExists reports whether a candidate Handle is already in use, so GenerateHandle/NewHandle can retry on collision without this package depending on any supervisor registry type.
type Identity ¶
Identity groups the three immutable facts that exist for a supervised process from the moment it is admitted: its opaque capability Handle, its authority Owner (SessionID + LoopID), and its audit-only Origin (the creating tool execution). None of these fields ever change after admission; only a process's State (state.go) transitions over its lifetime.
type ImmutableIdentityChangedError ¶
type ImmutableIdentityChangedError struct {
Handle Handle
}
ImmutableIdentityChangedError reports an attempted manifest update that would change the immutable Handle, Owner, or Origin recorded for a process (identity.go: "None of these fields ever change after admission").
func (*ImmutableIdentityChangedError) Error ¶
func (e *ImmutableIdentityChangedError) Error() string
type Lease ¶
type Lease interface {
Release() error
}
Lease is Task 8's minimal placeholder for the Harness lifetime workspace lease a caller acquires before calling Start (spec "Workspace coordination": "Tools acquires the matching Harness lifetime workspace lease" between PrepareProcess and Start). Task 8 does not wire Harness end-to-end and does not import any concrete Harness lease type; Start only needs "the resource the caller already acquired that must be released on every exit path". The method is named Release, not Close, so a call site reads unambiguously against PreparedProcess.Close (which releases the preparation, not the lease) -- Task 15/19 replaces this interface with (or adapts it to) the real Harness lease type.
type LifecycleEventIDChangedError ¶
LifecycleEventIDChangedError reports an attempted manifest update that would reassign an already-allocated stable lifecycle EventID or completion CommandID (spec "Manifests and durability": these IDs are "allocated and persisted before publication", so retries must always reuse the same ID).
func (*LifecycleEventIDChangedError) Error ¶
func (e *LifecycleEventIDChangedError) Error() string
type LifecycleEventIDs ¶
type LifecycleEventIDs struct {
Started uuid.UUID
Backgrounded uuid.UUID
Completed uuid.UUID
Lost uuid.UUID
CommandID uuid.UUID
}
LifecycleEventIDs are the stable, per-kind identifiers a manifest allocates and persists before the corresponding Harness lifecycle event or completion notification may be published (spec "Manifests and durability": "stable lifecycle EventIDs and completion-notification CommandID allocated before publication"). The zero uuid.UUID means "not yet allocated"; once a field is non-zero, ManifestStore.Save rejects any attempt to change it (LifecycleEventIDChangedError), so retries always reuse the same ID.
type Manifest ¶
type Manifest struct {
Identity
Command CommandMetadata
Access AccessMode
TTY bool
State State
CreatedAt time.Time
StartedAt *time.Time
FinishedAt *time.Time
Deadline *time.Time
Cursors SpoolCursors
Result Result
Events LifecycleEventIDs
// CompletionPublished is a monotonically increasing marker of the last
// successfully attempted completion publication. It exists purely to
// skip a redundant republish attempt; it is never the deduplication
// boundary (spec "Manifests and durability": "the completion-published
// marker avoids needless retries but is not required for at-most-once
// journal state" — that boundary is the Harness durable journal, added
// in a later phase).
CompletionPublished int64
// contains filtered or unexported fields
}
Manifest is the durable record Tools atomically persists for one supervised process before returning its handle, and updates for the rest of the process's lifetime (spec "Manifests and durability"). Its exported fields are a plain, ergonomic value type in the style of Config; a ManifestStore is responsible for enforcing the monotonic and terminal-immutability invariants across successive Save calls — Manifest itself only validates that a single value is internally well-formed (Validate).
func NewManifest ¶
func NewManifest(id Identity, cmd CommandMetadata, access AccessMode, tty bool, createdAt time.Time, deadline *time.Time) Manifest
NewManifest builds the initial Manifest for a newly admitted process: State StateStarting, no started/finished/deadline timestamps unless deadline is non-zero, and zero cursors, result, and lifecycle IDs.
func (Manifest) Validate ¶
Validate reports whether m is internally well-formed: required identity fields are present, State/Access belong to their closed domains, and the timestamp/result fields are consistent with State (spec "Manifests and durability" invariants; a violation is exactly what Load reports as CodeManifestCorrupt for a value read from disk, and what Save rejects for a value a caller is trying to persist for the first time).
type ManifestStore ¶
type ManifestStore struct {
// contains filtered or unexported fields
}
ManifestStore persists one Manifest per process Handle as versioned JSON beneath a private resource-root directory (spec "Manifests and durability"). It never writes inside the workspace: root is an explicit, caller-supplied directory dedicated to process resource storage (spec "Supervisor lifetime": "The resource root is never the workspace").
func NewManifestStore ¶
func NewManifestStore(root string) *ManifestStore
NewManifestStore returns a ManifestStore rooted at root. root must already exist; ManifestStore never creates it.
func (*ManifestStore) Delete ¶
func (s *ManifestStore) Delete(h Handle) error
Delete removes the on-disk manifest for h, if any (spec "Eviction deletes the manifest and spool atomically from the supervisor's perspective" -- Task 8D's terminal-LRU retention is Delete's only caller today). Delete is idempotent: deleting a handle that has no manifest file -- already deleted, or never written -- is a no-op rather than an error, mirroring Spool.Remove's idempotence (spool.go).
func (*ManifestStore) Load ¶
func (s *ManifestStore) Load(h Handle) (Manifest, error)
Load reads and validates the manifest for h. A missing file reports CodeNotFound. Malformed JSON, an unrecognized format version, or a value that fails Manifest.Validate all report CodeManifestCorrupt.
func (*ManifestStore) Save ¶
func (s *ManifestStore) Save(m Manifest) error
Save validates m, then atomically persists it (spec "Manifests and durability": "write-new, sync, and atomic replace semantics"). If a manifest already exists for m.Handle, Save additionally enforces that the update does not change immutable identity, move State outside an approved state-machine edge, change the Result of an already-terminal manifest, move Cursors or CompletionPublished backward, or reassign an already-allocated lifecycle EventID/CommandID. Save refuses to overwrite an existing manifest that itself fails to load (CodeManifestCorrupt) rather than silently discarding evidence of the corruption.
type NonMonotonicUpdateError ¶
NonMonotonicUpdateError reports a manifest update that would move a monotonic field backward (spec "Manifests and durability": "State and cursor metadata never move backward"). Field names the offending value using its JSON key.
func (*NonMonotonicUpdateError) Error ¶
func (e *NonMonotonicUpdateError) Error() string
type Origin ¶
Origin is the immutable, audit-only provenance of the tool execution that created a process. It is recorded for traceability but deliberately carries no authority: every follow-up tool invocation necessarily has its own new ToolExecutionID, so comparing Origin against a follow-up call would reject every legitimate one. Authorization always compares Owner, never Origin (spec "Identity and authorization": "The originating Bash ToolExecutionID is stored immutably as audit provenance, but it is not compared to the execution ID of ProcessOutput, ProcessInput, or ProcessStop").
type Owner ¶
Owner is the immutable authority over a supervised process: the session and loop that created it (spec "Identity and authorization": "Each process has an immutable authority owner: SessionID + LoopID + ProcessID"). Every follow-up ProcessOutput, ProcessInput, or ProcessStop call is authorized by an exact match against both fields — never against Origin. See Origin for why the two are kept separate.
type ProcessInputTool ¶
type ProcessInputTool struct {
// contains filtered or unexported fields
}
ProcessInputTool implements the mutating ProcessInput tool over a session's shared Supervisor. supervisor and owner are resolved once, by the caller that constructs this value (mirrors ProcessOutputTool) -- ProcessInputTool itself never touches a tool.SessionResourceRegistry.
func NewProcessInput ¶
func NewProcessInput(supervisor *Supervisor, owner Owner) *ProcessInputTool
NewProcessInput constructs a ProcessInputTool bound to the session's shared supervisor and this tool's immutable process-authority owner. A nil supervisor is retained as a construction error and fails every call closed, mirroring NewProcessOutput's initErr convention.
func (*ProcessInputTool) Info ¶
Info returns ProcessInput's self-description. Name MUST equal "ProcessInput".
func (*ProcessInputTool) InvokableRun ¶
func (t *ProcessInputTool) InvokableRun(ctx context.Context, _ string) (*tool.ToolResult, error)
InvokableRun executes the PREPARED artifact bound to this call. It never reparses argsJSON. A missing/cross-owner handle, a terminal target process, or a nil live process all render the single bare-error shape (ProcessID + Error) without ever reaching applyOperations; every other outcome -- success or a mid-operation failure -- goes through applyOperations under this handle's serialization lock and then renders the same cursor-addressed snapshot shape ProcessOutput renders (readOne, reused verbatim from output_tool.go).
func (*ProcessInputTool) PrepareCall ¶
func (t *ProcessInputTool) PrepareCall(_ context.Context, _ uuid.UUID, argsJSON string) (tool.Request, tool.PreparedArtifact, error)
PrepareCall decodes, validates, and normalizes one ProcessInput call and freezes the result into a sealed processInputArtifact. Every argument is validated HERE; InvokableRun never re-parses argsJSON. The emitted Request carries no Requirements: writing to a process the caller already owns needs no new gate decision (spec "Identity and authorization": follow-up operations do not re-run the original Bash gate).
type ProcessOutputTool ¶
type ProcessOutputTool struct {
// contains filtered or unexported fields
}
ProcessOutputTool implements the read-only ProcessOutput tool over a session's shared Supervisor. supervisor and owner are resolved once, by the caller that constructs this value (see this file's package doc comment) -- ProcessOutputTool itself never touches a tool.SessionResourceRegistry.
func NewProcessOutput ¶
func NewProcessOutput(supervisor *Supervisor, owner Owner) *ProcessOutputTool
NewProcessOutput constructs a ProcessOutputTool bound to the session's shared supervisor and this tool's immutable process-authority owner. A nil supervisor is retained as a construction error and fails every call closed, mirroring bash.BashTool's initErr convention -- ProcessOutput never panics on a missing dependency.
func (*ProcessOutputTool) Info ¶
Info returns ProcessOutput's self-description. Name MUST equal "ProcessOutput".
func (*ProcessOutputTool) InvokableRun ¶
func (t *ProcessOutputTool) InvokableRun(ctx context.Context, _ string) (*tool.ToolResult, error)
InvokableRun executes the PREPARED artifact bound to this call. It never reparses argsJSON. A wait: any|all call first blocks (bounded by timeout_ms, if any, and always by ctx) until its combinator's condition is already satisfied or becomes satisfied; the wait's own outcome (including a timeout or ctx cancellation) is never surfaced as a call failure -- "the wait timeout affects only the output call, never the process" (spec) -- InvokableRun always proceeds to render whatever is available once the wait attempt returns. Every per-process failure (a missing/cross-owner handle, a cursor beyond total_bytes) renders as that one entry's own "error" field, never as a whole-call failure or a Go error.
func (*ProcessOutputTool) PrepareCall ¶
func (t *ProcessOutputTool) PrepareCall(_ context.Context, _ uuid.UUID, argsJSON string) (tool.Request, tool.PreparedArtifact, error)
PrepareCall decodes, validates, and normalizes one ProcessOutput call and freezes the result into a sealed processOutputArtifact. Every argument -- process_id/process_ids exclusivity and shape, cursor, limit_bytes, encoding, wait, and timeout_ms -- is validated HERE; InvokableRun never re-parses argsJSON. The emitted Request carries no Requirements: reading a process the caller already owns needs no new gate decision (spec "Identity and authorization": follow-up operations do not re-run the original Bash gate).
type ProcessStopTool ¶
type ProcessStopTool struct {
// contains filtered or unexported fields
}
ProcessStopTool implements the mutating ProcessStop tool over a session's shared Supervisor. supervisor and owner are resolved once, by the caller that constructs this value (mirrors ProcessOutputTool/ ProcessInputTool) -- ProcessStopTool itself never touches a tool.SessionResourceRegistry.
func NewProcessStop ¶
func NewProcessStop(supervisor *Supervisor, owner Owner) *ProcessStopTool
NewProcessStop constructs a ProcessStopTool bound to the session's shared supervisor and this tool's immutable process-authority owner. A nil supervisor is retained as a construction error and fails every call closed, mirroring NewProcessOutput/NewProcessInput's initErr convention.
func (*ProcessStopTool) Info ¶
Info returns ProcessStop's self-description. Name MUST equal "ProcessStop".
func (*ProcessStopTool) InvokableRun ¶
func (t *ProcessStopTool) InvokableRun(ctx context.Context, _ string) (*tool.ToolResult, error)
InvokableRun executes the PREPARED artifact bound to this call. It never reparses argsJSON. A missing/cross-owner handle or a nil live process on a non-terminal entry both render the single bare-error shape (ProcessID + Error) without ever calling Signal. An already-terminal entry is rendered from its existing manifest without ever calling Signal (idempotence). Otherwise the requested mode's signal/escalation/confirm sequence runs, and the result is either a bare teardown_failed error (a Signal call itself failed) or this process's current snapshot -- terminal only if this call's own wait actually observed the confirmed terminal manifest.
func (*ProcessStopTool) PrepareCall ¶
func (t *ProcessStopTool) PrepareCall(_ context.Context, _ uuid.UUID, argsJSON string) (tool.Request, tool.PreparedArtifact, error)
PrepareCall decodes, validates, and normalizes one ProcessStop call and freezes the result into a sealed processStopArtifact. Every argument is validated HERE; InvokableRun never re-parses argsJSON. The emitted Request carries no Requirements: stopping a process the caller already owns needs no new gate decision (spec "Identity and authorization": follow-up operations do not re-run the original Bash gate).
An omitted grace_ms defaults to t.supervisor.cfg.GracefulShutdownPeriod -- the exact same duration Supervisor.Shutdown's own escalation uses (config.go's GracefulShutdownPeriod doc comment: "how long supervisor shutdown and ProcessStop's terminate mode wait before escalating to kill") -- rather than a fixed constant, so a session configured with a different grace period gets a consistent default across both paths.
type Reader ¶
type Reader interface {
Read(cursor int64, maxBytes int) (data []byte, nextCursor int64, gap bool, err error)
}
Reader is the minimal cursor-addressed read surface render.go needs from a byte store. Both *Buffer (buffer.go) and *Spool (spool.go) already implement this exact method signature, so render.go depends on neither concretely: a caller passes whichever store currently holds the requested cursor range. Routing between the in-memory window and the disk spool (and any owner authorization) is Task 8's supervisor's job, not this package's rendering logic.
type RestoreError ¶
RestoreError reports one persisted process resource that Restore could not reconcile -- most commonly a manifest that fails to load (manifest.go's CodeManifestCorrupt) or whose disk spool fails to reopen (CodeSpoolCorrupt). Restore isolates and reports every such failure rather than aborting its whole scan, so one corrupt entry never hides every other healthy one (spec combined-acceptance: "corrupt entries are isolated and reported without hiding healthy entries").
func (*RestoreError) Error ¶
func (e *RestoreError) Error() string
func (*RestoreError) Unwrap ¶
func (e *RestoreError) Unwrap() error
Unwrap returns the underlying reconciliation failure, so errors.As/errors.Is can see through a RestoreError to a wrapped *Error (e.g. CodeManifestCorrupt).
type RestoreReport ¶
type RestoreReport struct {
Reconciled []Handle
Errors []RestoreError
}
RestoreReport is Restore's summary of one scan of the resource root: every Handle it successfully reconciled and registered into s.entries, and every Handle it could not (see RestoreError).
type Result ¶
type Result struct {
// ExitCode is set only when State == StateExited (spec "Durable events
// and notifications": "only completed/exited requires an exit code").
ExitCode *int
// Reason is the closed lifecycle-event reason (spec "Durable events and
// notifications" table), e.g. "exited", "failed", "timed-out",
// "interrupted", "terminated", "killed", "lost-on-restore".
Reason string
}
Result carries the terminal outcome fields recorded once a process reaches a terminal State (spec "Manifests and durability": "terminal result fields when complete"). It is the zero value for any non-terminal manifest.
type SafeTextResult ¶
type SafeTextResult struct {
// Output is the normalized, capped, model-visible text.
Output string
// StartCursor is the cursor Read was called with (before any
// gap adjustment).
StartCursor int64
// NextCursor is the exclusive cursor immediately after the bytes Read
// actually returned (Reader's own nextCursor, unaffected by the
// safe-text cap below).
NextCursor int64
// Gap reports whether StartCursor fell before the earliest retained
// byte; when true, Output begins at the earliest retained byte rather
// than at StartCursor, exactly as Reader.Read documents.
Gap bool
// Normalized reports whether normalization changed anything: invalid
// UTF-8 was replaced, or a control/escape sequence was removed. Safe
// pass-through text (Step 2's "safe text unchanged" case) reports
// false.
Normalized bool
// Binary reports whether the pre-normalization bytes looked like
// binary data (safetext.LooksBinary), so a caller can prefer routing
// this read through base64/Artifact instead of showing Output inline.
Binary bool
// Artifact is always populated, independent of Binary, so a caller can
// retrieve the exact raw bytes of this same cursor range later via a
// base64 read, even for output that rendered safely as inline text.
Artifact Artifact
}
SafeTextResult is the safe-text render outcome for one bounded, cursor-addressed read (spec "ProcessOutput API" result shape, the subset render.go owns: output/start_cursor/next_cursor/gap/normalized/binary/ artifact). Manifest-derived fields such as total_bytes, status, and exit_code belong to the future supervisor-facing ProcessOutput tool (Task 16), not to this task's render/encode logic.
func RenderSafeText ¶
func RenderSafeText(r Reader, handle Handle, cursor int64, maxBytes int, capBytes int64) (SafeTextResult, error)
RenderSafeText reads up to maxBytes of retained output from r starting at cursor -- sharing Reader's exact gap and cursor-ahead Read semantics -- normalizes it through a one-shot safetext.Normalizer, and caps the normalized text at capBytes (a non-positive capBytes defaults to DefaultMaxInlineResultBytes) without splitting a replacement sequence (safetext.Truncate).
RenderSafeText normalizes each call's byte window independently: a terminal escape sequence that happens to be split exactly at this read's byte boundary is safely dropped (never leaked as raw bytes) but not reconstructed across separate RenderSafeText calls, unlike safetext.Normalizer's own cross-call carry-over (normalize_test.go), which this function does not use across calls. Reconstructing a sequence split across two different ProcessOutput polls of the same process would require a per-process *safetext.Normalizer threaded through the supervisor across polls; that is out of this task's scope (see the task doc: render.go accepts already-resolved byte sources and does not wire supervisor plumbing) and is flagged here for the phase-gate reviewer to weigh in on for Task 8/16.
type Spool ¶
type Spool struct {
// contains filtered or unexported fields
}
Spool is the bounded, durable, cursor-addressed disk retention window for one process's combined stdout+stderr byte stream (spec "Output capture and storage"). It stores only the currently retained window plus a monotonically increasing TotalBytes counter; an append that would exceed the configured ceiling drops the oldest retained bytes rather than blocking or failing (spec: "The spool is a bounded retention window, not a hard cap on the process"). Every Append durably persists the new retained window via atomicfile.Replace before returning success, so the spool file is never left in a partially-written state that a concurrent reader could observe.
A Spool is safe for concurrent use by multiple goroutines.
func OpenSpool ¶
OpenSpool opens (or, if none exists yet, prepares to create) the spool for process h beneath root. ceiling bounds the retained window in bytes; a non-positive ceiling defaults to DefaultMaxProcessSpoolBytes (process/ config.go). OpenSpool never creates the on-disk file itself — the file is created by the first Append.
A truncated or otherwise inconsistent on-disk spool (malformed JSON, an unrecognized version, impossible cursor bounds, or a payload whose length does not match its declared retained window) is reported as CodeSpoolCorrupt.
func (*Spool) Append ¶
Append durably adds data to the end of the combined output stream and returns the resulting global cursor (equal to TotalBytes after the append). Append order determines the global cursor: cursors are assigned strictly in the order Append is called, regardless of whether the bytes originated from stdout or stderr.
If retaining data would exceed the configured ceiling, Append drops exactly enough of the oldest retained bytes to fit and still succeeds; TotalBytes keeps counting every byte ever appended, independent of what remains retained. Append never fails, blocks, or refuses data because of volume — a process is never terminated for output volume (spec).
func (*Spool) Close ¶
Close marks the Spool closed, rejecting further Append/Read calls. It does not remove the on-disk file. Close is idempotent: calling it more than once, including after Remove, is a no-op.
func (*Spool) Read ¶
func (s *Spool) Read(cursor int64, maxBytes int) (data []byte, nextCursor int64, gap bool, err error)
Read returns up to maxBytes of retained output starting at cursor, the exclusive cursor immediately after the returned data (nextCursor), and whether cursor fell before the earliest retained byte (gap). When gap is true, the returned data begins at the earliest retained byte rather than at cursor, exactly as the spec describes for both the in-memory window and the disk spool. A non-positive maxBytes returns every retained byte from the (possibly gap-adjusted) start.
A cursor beyond TotalBytes reports CodeCursorAhead. A negative cursor reports CodeInvalidArguments.
func (*Spool) Remove ¶
Remove closes the Spool and deletes its on-disk file, if any. Remove is idempotent: calling it more than once, or calling it when no file was ever written (e.g. a process that produced no output), is a no-op rather than an error.
func (*Spool) RetainedFrom ¶
RetainedFrom reports the cursor of the earliest byte currently retained on disk.
func (*Spool) TotalBytes ¶
TotalBytes reports the monotonically increasing count of every byte ever appended to the spool, independent of what remains retained.
type SpoolCursors ¶
type SpoolCursors struct {
// TotalBytes is the monotonically increasing count of every byte ever
// appended to the spool, independent of what remains retained.
TotalBytes int64
// RetainedFrom is the cursor of the earliest byte still retained on
// disk; bytes before it have been dropped by ceiling truncation.
RetainedFrom int64
}
SpoolCursors is a manifest's durable snapshot of its disk spool's cursor bounds (spec "Manifests and durability": "spool metadata and cursor bounds"), mirroring what a live Spool (spool.go) tracks so a manifest alone reports accurate bounds without reopening the spool file.
type State ¶
type State string
State is one of the externally visible supervised-process lifecycle states (spec "State machine").
const ( StateStarting State = "starting" StateRunning State = "running" StateExited State = "exited" StateFailed State = "failed" StateTimedOut State = "timed_out" StateInterrupted State = "interrupted" StateTerminated State = "terminated" StateKilled State = "killed" StateLostOnRestore State = "lost_on_restore" )
The closed set of externally visible states.
type StorageCeiling ¶
StorageCeiling bounds one supervised process's in-memory rolling window and disk spool retention window (mirrors Config's MaxProcessInMemoryBytes/MaxProcessSpoolBytes pair, and buffer.go's NewBuffer / spool.go's OpenSpool ceiling parameters). It is supplied per Start call rather than read from the Supervisor's Config, so a caller (Bash) can request a smaller window for one process; a non-positive field falls back to the Supervisor's configured per-process default (reserveQuota). Reserving InMemoryBytes/SpoolBytes against the Supervisor's aggregate ceilings before Start is this task's realization of the combined-acceptance text's "memory" and "spool" quotas.
type Supervisor ¶
type Supervisor struct {
// contains filtered or unexported fields
}
Supervisor is the runner-free process supervisor described by the spec's "Supervisor lifetime" and "Workspace coordination" sections: it never calls tool.AsyncProcessRunner.PrepareProcess itself, and NewSupervisor deliberately does not accept a tool.AsyncProcessRunner at all. A caller (Task 8 does not implement this caller; it is the Bash tool built in Phase 4, per the spec's "Only Bash owns execution authority") resolves a runner, calls PrepareProcess, acquires the matching workspace Lease, and hands the resulting tool.PreparedProcess to Start. Supervisor then owns admission -- reserving quotas and consuming the single-use preparation (this microtask, 8A) -- and, starting in Task 8B, durable handoff, stream drain, terminal arbitration, and retention.
A Supervisor is safe for concurrent use by multiple goroutines.
func NewSupervisor ¶
func NewSupervisor(cfg Config, manifests *ManifestStore, spoolRoot string, lifecycle lifecycleSink, notifications completionNotifier) (*Supervisor, error)
NewSupervisor returns a Supervisor governed by cfg (normalized; see Config.Normalize), persisting manifests to manifests and, from Task 8B on, opening spools beneath spoolRoot. lifecycle and notifications are the two narrow SessionResourceServices capabilities described by the spec's "Supervisor lifetime" section; see the Supervisor.lifecycle/notifications field doc for why they are accepted here even though 8A never calls either. NewSupervisor deliberately does not accept a tool.AsyncProcessRunner: the supervisor is runner-free by construction, unable to call PrepareProcess even if some future caller wanted it to.
func (*Supervisor) Restore ¶
func (s *Supervisor) Restore(ctx context.Context) (RestoreReport, error)
Restore is the session-restore entrypoint (spec "Manifests and durability": "On normal session restore ..."). It scans every manifest durably persisted beneath the Supervisor's ManifestStore root and reconciles each one into a queryable, non-running s.entries record:
- a manifest already in a terminal State (state.go's State.Terminal()) is reopened as-is: its completed output remains readable through its Spool, with no live tool.Process, goroutine, lease, or quota reservation (TestRestoreCompletedOutput);
- a manifest still in StateStarting or StateRunning means the supervisor process that was running it no longer exists -- a session restore implies exactly that (spec: "a manifest marked running or starting becomes lost_on_restore"). It is durably transitioned to the terminal StateLostOnRestore first (markLostOnRestore), publishing the lost lifecycle event and completion notification through the manifest's already-persisted stable LifecycleEventIDs.Lost/CommandID -- never a freshly minted ID (TestRestoreRunningBecomesLost, TestRestorePublicationCrashRetriesStableID) -- and only then reopened the same way as an already-terminal manifest.
Restore is intended to be called once, immediately after NewSupervisor and before any Start/Wait call, as the caller's session-restore step; it is not a periodic reconciliation loop, and it never touches quota bookkeeping (runningByLoop/runningBySession/reservedMemoryBytes/reservedSpoolBytes): nothing here was ever actually reserved by this process instance, and nothing will ever call releaseQuota for a restored entry.
Restore never constructs, obtains, or references a tool.Process, tool.PreparedProcess, or tool.AsyncProcessRunner: there is no live process handle for a restored entry, by construction, so there is no signal-capable value anywhere in this reconciliation path that could ever be handed a persisted PID (spec: "Restore never signals a PID recovered from persisted metadata"; TestRestoreNeverSignalsPersistedPID). The manifest's unexported osMetadata field is never read here at all -- see manifest.go's osMetadata doc comment.
A manifest that fails to load, or whose disk spool fails to reopen, does not abort the scan: it is recorded in the returned RestoreReport.Errors and every other manifest is still reconciled normally.
func (*Supervisor) Shutdown ¶
func (s *Supervisor) Shutdown(ctx context.Context) error
Shutdown implements the spec's "Supervisor lifetime" coordinated shutdown sequence: close admission to new processes, concurrently request graceful termination of every currently running process tree, escalate to a forceful kill for any tree still running once cfg.GracefulShutdownPeriod elapses, confirm every tree has exited, and return any teardown failure to the caller.
Shutdown closes admission (closeAdmission) as its very first step -- before requesting termination of a single process -- so a concurrent Start call is rejected immediately with CodeSupervisorShuttingDown even while Shutdown's later termination/escalation steps are still in flight (TestShutdownClosesAdmissionBeforeStop).
Every currently running entry (snapshotRunningEntries) is signaled tool.ProcessSignalTerminate concurrently, one goroutine per entry; an entry that has not confirmed exit within cfg.GracefulShutdownPeriod of that request is then signaled tool.ProcessSignalKill (TestShutdownEscalatesAndConfirmsTrees). Shutdown waits on each entry's exited channel -- not done -- for that confirmation: see entry.go's exited field doc comment for why a slow or backpressured completion notification must never delay this step (combined-acceptance: "notification backpressure cannot block terminalization"). A signaled entry's actual terminal-state computation and manifest write are never bypassed or duplicated here -- run's existing natural Wait()-return path (entry.terminalize) is what every signal ultimately drives, exactly as it already does for an unforced exit.
Shutdown is idempotent and safe to call concurrently: only the first call's invocation actually runs the shutdown sequence (sync.Once); every concurrent or later caller receives that exact same result (TestShutdownConcurrentCallersShareResult). Only the first caller's ctx is ever used -- every other caller's ctx argument is ignored, exactly like every other input to a no-op Do call.
A teardown failure -- a Signal call that itself returns an error -- doesn't stop the affected entry from reaching a terminal state: its run goroutine still classifies whatever outcome its live tool.Process.Wait ultimately reports and terminalizes normally. Shutdown only aggregates every such Signal failure into its own returned error, a *Error wrapping CodeTeardownFailed (TestShutdownTeardownFailureRetainsAuthority) -- Shutdown retains authority over every process regardless of a teardown hiccup; it never loses track of one.
Steps the ordered sequence describes but that need no new code here: terminal manifests are already flushed and workspace leases are already released by entry.doTerminalize (Task 8C), which every signaled entry still reaches through its own run goroutine; any caller still blocked in Wait is already released once an entry's done channel closes (Task 9A's existing generation/done wake mechanism), no new mechanism required. Closing storage handles is a deliberate no-op: neither ManifestStore nor Spool has (or should have) a Supervisor-wide Close of its own -- a process's completed output must remain readable through its Spool after Shutdown returns, exactly as Restore already relies on for a clean (non-lost) reconciliation of a shutdown-terminated process (TestSupervisorIntegrationShutdownAndRestore).
func (*Supervisor) Start ¶
func (s *Supervisor) Start( ctx context.Context, owner Owner, origin Origin, prepared tool.PreparedProcess, lease Lease, sink lifecycleSink, observations observationInvalidator, ceiling StorageCeiling, yield YieldSettings, ) (Handle, error)
Start begins supervising one already-prepared process. The caller has already resolved an AsyncProcessRunner, called PrepareProcess, and acquired the matching workspace lease (spec "Workspace coordination"); Start only ever consumes what it is handed -- it never calls PrepareProcess itself. owner and origin are recorded verbatim as the new process's Identity (identity.go); prepared is the single-use preparation this call consumes; lease is released on every path that does not end with a registered entry owning it; sink, observations, ceiling, and yield are recorded on the entry for later microtasks to use.
Start reserves every quota reserveQuota enforces before calling prepared.Start (TestSupervisorReservesQuotaBeforeStart), at most once, exactly matching PreparedProcess.Start's own single-use contract. It then atomically persists this process's manifest in state StateStarting -- via s.manifests, a real durable ManifestStore -- before ever calling prepared.Start, so a Handle can never be returned for a process that has no durable record (TestSupervisorPersistsBeforeReturningHandle; spec "Manifests and durability": "Before returning a process handle, Tools atomically persists a manifest"). This requires minting the Handle (and therefore checking handleExists) before prepared.Start is called, not after, unlike a Handle-agnostic admission flow would.
If prepared.Start fails, Start releases every reservation it made, releases lease, closes prepared -- idempotent per the Harness PreparedProcess contract -- and transitions the already-persisted StateStarting manifest directly to StateFailed (there is no live entry on this path, so this is a plain synchronous Save, not entry.terminalize's compare-and-set: Start's own call sequence is already the only writer of this manifest at this point, and no concurrent caller can race it) before returning a *Error wrapping CodeSpawnFailed (TestSupervisorStartFailureReleasesQuota). If a quota is already exhausted, Start returns a *Error wrapping CodeProcessQuotaExceeded without ever calling prepared.Start or prepared.Close, and without minting a Handle or writing any manifest (TestSupervisorRejectsSessionAndLoopQuota).
Once prepared.Start succeeds, Start persists the manifest's StateRunning transition, opens this process's in-memory Buffer and durable Spool (both sized from the exact quota reservation -- see reservation's doc comment), registers the entry, and starts the entry's single wait/activity/drain goroutine (entry.go's run) on a lifetime-scoped context that is deliberately independent of ctx (see run's doc comment for why). Start returns the freshly minted Handle only after the entry is registered and its goroutine has been started (TestSupervisorDrainsOrderedStreams, TestSupervisorSpoolCeilingDropsOldest).
Start allocates this process's stable LifecycleEventIDs (newLifecycleEventIDs) and records them on the very first manifest it persists, so they are already durably in place before the process can ever reach a terminal state (TestSupervisorPublishesStableLifecycleIDs; spec "Manifests and durability"). Start itself does not arbitrate a terminal state for a process that reaches one after prepared.Start succeeds -- that is entry.terminalize's one-shot compare-and-set (entry.go), driven by run's natural Wait() return today and, starting in Task 9C, also by an explicit stop request, a deadline timeout, or supervisor shutdown.
If closeAdmission has already been called, Start rejects admission immediately with a *Error wrapping CodeSupervisorShuttingDown, before reserving any quota or touching prepared at all (TestSupervisorShutdownRejectsAdmission).
A prepared.Start failure is classified by classifyStartError, below, rather than unconditionally reported as CodeSpawnFailed: a real AsyncProcessRunner adapter (e.g. the product composition root's process_adapter.go) reports a more specific tool.ProcessError classification for a handful of documented, distinct failure modes (lifetime containment unavailable, PTY unavailable), and that specific reason must survive through this layer for bash/supervised.go's own classifyProcessError to see it rather than a generic spawn failure that already lost it here.
func (*Supervisor) Wait ¶
func (s *Supervisor) Wait(ctx context.Context, owner Owner, kind WaitKind, targets []WaitTarget) ([]WaitStatus, error)
Wait reports (poll) or waits for (any/all) new output or a terminal transition across targets, scoped to entries owner can see.
WaitPoll returns the current WaitStatus for every target immediately: it never blocks and never even inspects ctx (TestWaitPollReturnsImmediately).
WaitAny and WaitAll block until their respective condition is satisfied or ctx.Done() fires, returning ctx.Err() on the latter (TestWaitCancelRemovesWaiter). Neither mode polls or sleeps to notice a change: both are woken directly by appendChunk's generation bump (entry.go's bumpGeneration) or by an entry's done channel closing at its terminal transition (TestWaitAnyWakesOnAppend, TestWaitAllRequiresEveryEntry).
A blocking call (any/all) first reserves one waiter slot against cfg.MaxPendingWaiters for owner.SessionID (acquireWaiterSlot), releasing it unconditionally on every return path -- including cancellation, so a canceled wait never leaks its slot (TestWaitCancelRemovesWaiter). If the quota is already exhausted, Wait returns a *Error wrapping CodeOutputQuotaExceeded immediately, before registering any watcher. Poll-mode calls never touch the quota: they neither reserve nor require a slot.
type SupervisorResource ¶
type SupervisorResource struct {
Supervisor *Supervisor
Manifests *ManifestStore
}
SupervisorResource adapts the shared *Supervisor (and its backing *ManifestStore) to tool.SessionResource, so it can be obtained through tool.SessionResourceRegistry.GetOrCreate by any of the four process-backed tool definitions (Bash's supervised path, ProcessOutput, ProcessInput, ProcessStop). GetOrCreate's factory runs exactly once per key: whichever caller's GetOrCreate reaches SupervisorResourceKey first determines the concrete tool.SessionResource value every later caller with that key receives back, including a caller in a different package. Exporting one shared type/factory here (rather than each consumer package keeping its own private wrapper) is what makes that safe: every caller, regardless of which one wins the race, type-asserts the resource to the same *SupervisorResource.
func (*SupervisorResource) Activate ¶
func (r *SupervisorResource) Activate(ctx context.Context, services tool.SessionResourceServices) error
Activate wires the real, validated tool.SessionResourceServices Harness's live session construction supplies into the shared Supervisor this resource wraps, and then performs this resource's session-restore reconciliation step. NewSupervisorResource itself constructs the Supervisor notification-free (nil lifecycle/notifications at NewSupervisor time, below) precisely because these real capabilities do not exist yet at factory time -- SessionResource's own contract late-binds them here, after the live session, hub, durable publisher, and notifier are ready (pkg/tool.SessionResource's doc comment). Activate adapts services' validated tool.ProcessLifecyclePublisher/tool.ProcessCompletionNotifier into this package's private lifecycleSink/completionNotifier shapes (lifecycle_bridge.go) and installs them on the live Supervisor (activateServices), which every subsequent Start call -- and so every admitted process's Start-time and terminal lifecycle publish and completion notify -- reads from that point on (supervisor.go's servicesLocked). services.Validate() rejects a nil or typed-nil service before either is installed, so a caller that mishandles construction can never leave the Supervisor half-wired.
Activate then calls Supervisor.Restore over this resource's own directory, exactly matching restore.go's own documented contract ("intended to be called once, immediately after NewSupervisor and before any Start/Wait call, as the caller's session-restore step"). Nothing else in this package or in bash ever called it: NewSupervisorResource always constructs a fresh, empty in-memory Supervisor regardless of whether its directory already holds manifests a PRIOR process durably left there (persisted sessions reuse the identical resource root across a real restart), so a real session restore silently discarded every previously known process -- completed output became permanently unreadable and a still-running manifest was never marked lost. This is Activate's own natural, single, guaranteed-once-before-any-use hook for that reconciliation (SessionResource's own contract: "late-binds live session services after construction AND RESTORE PLANNING" -- pkg/tool's doc comment), so it needs no new "is this a restore" signal of its own: Restore's own directory scan is a harmless no-op (an empty Reconciled list, no error) when this resource's directory has nothing to reconcile yet, i.e. for a genuinely new session -- see listManifestHandles' own os.IsNotExist handling. Restore runs AFTER activateServices, not before: a still-running manifest's lost-on-restore reconciliation (markLostOnRestore/publishLostOnRestore, restore.go) publishes through these exact services, and must not silently no-op against the construction-time nils.
type TerminalResultChangedError ¶
TerminalResultChangedError reports an attempted manifest update that would change the terminal Result of a manifest already in the same terminal State (spec "Manifests and durability": terminal result is immutable once set). This is a programming-invariant violation, not a stable model-facing code, so it is a plain typed Go error in the style of state.go's TransitionError rather than a *Error.
func (*TerminalResultChangedError) Error ¶
func (e *TerminalResultChangedError) Error() string
type TransitionError ¶
TransitionError reports an attempted state transition that is not an approved edge in the supervision state machine (including any transition out of a terminal state, or into an unrecognized state).
func (*TransitionError) Error ¶
func (e *TransitionError) Error() string
type WaitKind ¶
type WaitKind string
WaitKind selects how Supervisor.Wait observes a set of process handles (spec "ProcessOutput API": `"wait": "poll | any | all"`). This is Task 9A's generic waiter primitive: the future ProcessOutput tool (Task 16) translates its own `wait`/`cursor` arguments into a WaitKind and a set of WaitTarget values and calls Wait through the Supervisor; 9A does not build ProcessOutput itself.
const ( // WaitPoll returns every target's current status immediately, never // blocking and never consulting ctx. WaitPoll WaitKind = "poll" // WaitAny blocks until at least one target has advanced past its // supplied generation or become terminal, or ctx is done. WaitAny WaitKind = "any" // WaitAll blocks until every target has advanced past its supplied // generation or become terminal, or ctx is done. WaitAll WaitKind = "all" )
The closed set of wait kinds.
type WaitStatus ¶
type WaitStatus struct {
Handle Handle
// Generation is the entry's current generation counter.
Generation uint64
// Terminal reports whether the entry has reached a terminal state.
Terminal bool
// Found reports whether Handle named a live entry visible to the
// calling Owner. A Handle that does not exist and a Handle owned by a
// different Owner are deliberately indistinguishable here -- both
// report Found false and every other field at its zero value -- so a
// cross-owner probe can never be told apart from a missing one (spec
// "Identity and authorization"; mirrors Owner.Equal's doc comment).
Found bool
}
WaitStatus is Wait's per-target report.
type WaitTarget ¶
WaitTarget is one process a Wait call watches, paired with the generation the caller last observed for it. Generation is this package's own append-driven counter (entry.go's entry.generation), not a byte cursor: a caller with a byte cursor (Task 16's ProcessOutput) derives "have I already seen everything as of this cursor" itself, from a WaitStatus's Generation together with its own last-read output cursor, and only needs Wait to tell it "something changed, go look again" versus "nothing changed yet". The zero value (a target never observed before) blocks until the entry has appended at least once or is already terminal.
type YieldSettings ¶
type YieldSettings struct {
// Yield requests that the process detach to the background immediately
// rather than staying attached to the foreground Bash call.
Yield bool
}
YieldSettings carries one Start call's explicit backgrounding preference (spec "Bash API": a foreground call may explicitly yield to the background rather than staying attached and returning inline). Task 8 does not implement yield/backgrounded-handoff behavior yet -- that is part of Task 8B/8C's lifecycle-event work and, ultimately, the Bash tool built in Phase 4; this struct only reserves the field Start's signature commits to.