Documentation
¶
Overview ¶
Package worker hosts a single-session gofer daemon — the "a worker is a single-session daemon" realization from the M6 process-isolation design (docs/milestones/M6-process-isolation.md). A worker binds a per-session unix socket (path keyed by its session uuid), serves the EXISTING daemon wire (ACP v1 + gofer/* native) capped at one session, and announces its address to the parent process (the M6 router) two ways: an on-disk endpoint file (for the router's future adoption scan) and a single machine-readable handshake line printed to stdout before it begins serving (for fresh-spawn discovery).
A worker is detached from the router by design (design §3): the router spawns it with Setsid so it outlives a router restart. Everything above the SDK loop (pump, gate, journal, broker) runs here, so the router can be restarted without disturbing an in-flight turn.
Session-id pinning (design Option A) ¶
A worker's socket, endpoint file, and single-writer lock are all keyed by the SESSION uuid. But the worker does not itself mint that uuid — the router pre-generates it and passes it as --session so it can key those files BEFORE the worker starts. The worker therefore treats --session as REQUIRED and pins it as its session id via the SDK's runner.Options.SessionID seam, which the store adopts verbatim while entry ids keep coming from the store default.
Router restart survival (adoption) ¶
A detached worker outlives a router restart. The worker keeps running, keeps holding its unix socket, its <uuid>.lock, and its <uuid>.json endpoint file; the NEXT router start scans those endpoint files and re-adopts the still-alive worker by dialing its socket (see internal/router's adoptExistingWorkers). This endpoint file is that scan's advertisement — pid for the liveness probe, addr to dial, wire/binary version for the adopt/skew decision.
Handshake transport contract ¶
The router codes against this exact contract for fresh-spawn discovery:
- The worker writes exactly ONE handshake line — the JSON encoding of Handshake — to stdout, before it begins serving. Under the router's detached spawn stdout is redirected to the worker's log file, so the router discovers the line by scanning that file; in-process tests capture it via Options.Stdout/Options.Ready.
- Ordering is load-bearing: the worker binds its listener, THEN writes the handshake, THEN serves. The handshake's appearance means "ready to dial".
- Handshake.Addr is self-describing ("unix://<path>"); the router dials it verbatim with daemon.Dial (no token).
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Serve ¶
Serve hosts opts.Supervisor behind a single-session daemon on a per-session unix socket, advertises itself (endpoint file + Handshake line), and blocks serving the daemon wire until ctx is cancelled — then gracefully shuts the listener down, closes the supervisor, and clears its endpoint file + lock. It is the shared core of the `gofer session-worker` command and its in-process tests.
The startup sequence is load-bearing (design §3/§4):
- Compute the socket path; fail fast on its length-guard error BEFORE acquiring anything.
- Acquire the single-writer lock; daemon.ErrWorkerLocked means another live worker owns this session — exit.
- Remove any stale socket left by a crashed predecessor (safe: we hold the lock).
- Bind the unix socket.
- Write the endpoint file (the router's adoption-scan advertisement).
- Write the handshake (fresh-spawn discovery), THEN serve.
On a CLEAN exit (ctx cancelled) it removes the endpoint file and releases the lock. The lifecycle is deliberately asymmetric: an abnormal listener stop — and a crash, where no deferred code runs at all — leaves the <uuid>.lock, endpoint file, and socket behind; the router's adoption scan garbage-collects these stale artifacts (dead pid, or a dialed-refused socket).
Types ¶
type Handshake ¶
type Handshake struct {
// Addr is the worker's self-describing unix-socket address,
// "unix://<path>" (see [daemon.WorkerSocketPath]) — dialed verbatim by the
// router via daemon.Dial's unix:// scheme.
Addr string `json:"addr"`
// PID is the worker process id (os.Getpid()), for the router's reap/probe
// path.
PID int `json:"pid"`
// Version is the worker's build version (cmd/gofer's effectiveVersion),
// forward-useful for M6 version-skew routing. Omitted when empty.
Version string `json:"version,omitempty"`
}
Handshake is the single machine-readable line a worker prints to stdout, before it begins serving, so the spawning parent (the M6 router) can learn the address to dial. It is the JSON encoding of this struct on ONE line, and it is the FIRST thing the worker writes to stdout. The router scans lines (skipping any log lines interleaved under a detached spawn's shared stdio file) until it decodes one.
Addr is required; PID and Version are additive/forward-useful (PID lets the router reap/probe the worker; Version feeds the M6 skew-routing decision). The router imports and parses this exact type — treat the JSON tags as a wire contract.
type Options ¶
type Options struct {
// Supervisor is the single-session supervisor the worker hosts. The caller
// builds it (root/model/permissions/telemetry) and hands it over; Serve
// closes it on shutdown. Required. The caller is responsible for building it
// with a factory that sets runner.Options.SessionID so its one session adopts
// Session as its id.
Supervisor *supervisor.Supervisor
// Session is the pinned session uuid the router pre-generated (design Option
// A). It keys the worker's socket ([daemon.WorkerSocketPath]), endpoint file
// ([daemon.WorkerEndpointPath]), and single-writer lock
// ([daemon.WorkerLockPath]). REQUIRED: Serve fails fast if it is empty —
// there is no self-generated fallback, since that would desync the file
// keying from the pinned session id.
Session string
// DefaultModel is the model a session/new resolves to when the client
// supplies none — forwarded verbatim into daemon.Config.DefaultModel.
DefaultModel string
// Version is the worker's build version (cmd/gofer's effectiveVersion). It
// stamps all three of the worker's version advertisements: the handshake
// line's Version (see [Handshake]), the endpoint file's BinaryVersion (the
// router's cheap pre-dial hint), and gofer/hello's binaryVersion (the
// authoritative in-protocol handshake the router classifies skew from,
// design §6). Empty omits it from the handshake and reports an empty
// binaryVersion — which a router classifies as unknown-but-adoptable rather
// than failing.
Version string
// Logger receives the worker's structured logs. Nil discards them. Logs go
// to stderr in the cmd wiring; they must never reach Stdout, which carries
// only the handshake line.
Logger *slog.Logger
// Stdout receives the single handshake line. Nil defaults to os.Stdout.
// Kept as an injectable io.Writer so a test can capture and parse the
// handshake without os/exec. It MUST be unbuffered (os.Stdout and an
// io.Pipe both are): writeHandshake does not flush, so a bufio.Writer would
// strand the handshake and the router would hang waiting to read it.
Stdout io.Writer
// Ready, if non-nil, is invoked exactly once with the bound Handshake
// immediately after the listener binds and the handshake is written — the
// in-process test seam that lets a test learn the address (and drive a
// turn) without spawning a process. It runs before Serve blocks on ctx.
Ready func(Handshake)
}
Options configures Serve.