jobs

package
v0.23.1 Latest Latest
Warning

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

Go to latest
Published: Aug 17, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package jobs is memcode's background-job layer — the async extension of the shell lanes. A dev server / watcher / `docker compose up` never exits, so it can't run through the blocking bash path; it runs here: started detached, output captured to a capped ring buffer, status tracked, killable. Doctrine: foreground answers now; background jobs maintain temporal state; monitors turn waiting into memory.

Concurrency note: the registry is the runtime's first concurrent subsystem. All mutable Job state is guarded by Registry.mu; callers only ever see immutable View snapshots. Jobs use a process GROUP (Setpgid) so killing one reaps the whole tree (npm → node → …), and they run under a background context so they outlive the turn.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func RunForeground

func RunForeground(ctx context.Context, cmd *exec.Cmd) error

RunForeground runs cmd to completion in its OWN process group, killing the whole group if ctx is cancelled or times out — so a child (e.g. node under npm) can't hold the output pipe open and hang cmd.Wait() after the kill. Use this instead of exec.CommandContext + cmd.Run() for any foreground command that may spawn children: CommandContext kills only the direct child, which is why an un-grouped `next dev` made the agent turn unkillable (Esc/Ctrl-C/timeout couldn't free it). Returns ctx.Err() on cancel, else the command's exit error.

Types

type ForegroundOutcome

type ForegroundOutcome struct {
	Done     bool   // ran to completion within the time budget
	Killed   bool   // turnCtx was cancelled (Esc/Ctrl-C) — the whole group was reaped
	Exit     int    // exit code when Done (-1 if it couldn't start)
	Stdout   string // captured stdout when Done
	Stderr   string // captured stderr when Done
	Promoted *View  // non-nil: still running at the deadline, handed to a background report-back job
}

ForegroundOutcome is the result of RunForegroundOrPromote.

func RunForegroundOrPromote

func RunForegroundOrPromote(r *Registry, bgCtx, turnCtx context.Context, timeout time.Duration, command, cwd string) ForegroundOutcome

RunForegroundOrPromote runs command in its OWN process group and waits up to `timeout` for it to finish. Three outcomes:

  • it finishes → Done, with Exit + Stdout + Stderr (the old blocking bash path);
  • turnCtx is cancelled first (Esc/Ctrl-C) → Killed, the group reaped;
  • it's STILL running at the deadline → it is NOT killed. It's promoted to a tracked background job (watched under bgCtx, reporting its result back on completion) and returned via Promoted, so the turn stops blocking on it.

bgCtx MUST be the long-lived session context so a promoted job outlives the turn; turnCtx is the per-turn context that carries interrupt.

type Registry

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

Registry owns the live background jobs for a session.

func New

func New() *Registry

func (*Registry) DrainReports

func (r *Registry) DrainReports() []ShellReport

DrainReports returns and clears the finished report-back shell jobs (promoted commands), each exactly once. Called from the UI poll to hand results back to the model.

func (*Registry) Kill

func (r *Registry) Kill(id int) bool

Kill terminates a running job's process group. Returns whether a running job was killed.

func (*Registry) KillAll

func (r *Registry) KillAll()

KillAll terminates every running job — call on session end so nothing orphans.

func (*Registry) List

func (r *Registry) List() []View

List returns snapshots of all jobs, running first then most-recent.

func (*Registry) Running

func (r *Registry) Running() int

Running returns how many jobs are still running (for the footer "N shells").

func (*Registry) Tail

func (r *Registry) Tail(id, n int) (string, bool)

Tail returns the last n lines of a job's captured output.

type ShellReport

type ShellReport struct {
	ID      int
	Command string
	Status  Status
	Exit    int
	Output  string
}

ShellReport is a finished report-back shell job — a background shell or a promoted foreground command — owed back to the model. Drained by the UI poll and injected as a new turn.

type Status

type Status string
const (
	Running Status = "running"
	Exited  Status = "exited" // finished with exit 0
	Failed  Status = "failed" // finished non-zero
	Killed  Status = "killed" // we killed it (/kill or session end)
)

type SyncBuf

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

SyncBuf is a thread-safe output sink: unbounded while a command runs in the foreground (so a normal bash call's full output is captured, as the old strings.Builder did), then capped to its tail via Cap() if the command is promoted to a long-running background job.

func (*SyncBuf) Cap

func (b *SyncBuf) Cap(n int)

Cap bounds the buffer to its last n bytes and keeps it that way — called on promotion.

func (*SyncBuf) String

func (b *SyncBuf) String() string

func (*SyncBuf) Write

func (b *SyncBuf) Write(p []byte) (int, error)

type View

type View struct {
	ID      int
	Command string
	Status  Status
	Exit    int
	Started time.Time
	Ended   time.Time
}

View is an immutable snapshot of a job for callers (footer, /jobs, /tail).

func Start

func Start(r *Registry, ctx context.Context, command, cwd string) (View, error)

Start launches command (via the platform shell) in cwd as a detached background job and returns its View. ctx should be a LONG-LIVED context (session/background) — never a turn context, or the job dies when the turn ends. Every started job reports back on its own exit (reportBack=true): a finished poller hands its result to the model, and a dev server that exits did so unexpectedly — exactly when you want to hear about it. Jobs WE kill (/kill, session end) stay silent.

Jump to

Keyboard shortcuts

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