enginehost

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Sep 18, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

Documentation

Overview

Package enginehost is the process that keeps a conversation alive between connections.

`codeaf chat --host devbox` runs `ssh devbox codeaf engine` and speaks internal/remote's protocol over the pipes. In version 1 that process WAS the conversation: it opened the session, answered frames, and died with the pipe, so a closed laptop lid and a dropped wifi both ended a running turn. This package is the other half of version 2's answer — a host that outlives the pipe, holding the engine, while `codeaf engine` becomes a splice between the ssh pipes and a unix socket.

── THE GRAIN IS ONE HOST PER WORKSPACE, AND THE CHDIR LAW DECIDES IT ────────

A host holds one engine per open session, but every session it holds is about the SAME directory, and that is not an arbitrary carving. `codeaf engine` moves the process into the workspace before it assembles anything (cmd/codeaf engine.go), and that chdir is the only chdir in the tree — the process's own idea of where it is has to agree with the session config's. A host holding two workspaces would have to break that or lie about it. So the socket lives under a directory named for the workspace, a host is born in it, and a hello asking for a different one reaches a different host through a different socket. Nothing about the protocol changes; the routing happens before the first frame.

── NOTHING HERE IS REQUIRED ─────────────────────────────────────────────────

THE FALLBACK IS NOT OPTIONAL. A machine where the socket directory cannot be made, where the path is too long for a unix socket, where the lock cannot be taken, or where the host simply refuses to start must still take a remote session — as the version-1 engine did, on the pipe, with the welcome saying remote.Welcome.Persistent is false. Every door in this package is written to be allowed to fail: the caller reads the error as "not today" and serves the connection itself.

Index

Constants

View Source
const SocketLimit = 104

SocketLimit is the most bytes a unix socket path may weigh.

It is 104 rather than Linux's own 108 because THE SMALLEST LIMIT IS THE ONE THAT TRAVELS: macOS stops at 104, the same codeaf home can be shared over a network mount, and a host that worked on one machine and refused on another for a reason nobody could see would be worse than one honest refusal everywhere. Exceeding it is not a fault — CODEAF_HOME can be anywhere — so it is answered as "no host today" and the caller falls back to the pipe.

Variables

View Source
var ErrHostBusy = errors.New("engine host: this host is holding work in flight")

ErrHostBusy is a host that will not retire because it is holding work: a surface attached, a turn running, or a question waiting for an answer. It is not a fault and the caller must not treat it as one — the machine is doing exactly what somebody asked it to.

View Source
var ErrHostRunning = errors.New("engine host: another host already holds this workspace")

ErrHostRunning is Run finding that this workspace already has a host. It is not a failure and the caller must not print it: the machine is in exactly the state that was asked for, by another process.

View Source
var ErrNoHostAnswered = errors.New("engine host: no host answered")

ErrNoHostAnswered is a host that had somewhere to listen but did not answer before the birth wait ran out. It stays distinct from ErrSocketPathTooLong so the caller can tell a failed start from one that was never possible.

View Source
var ErrSocketPathTooLong = errors.New("engine host: the state path is too long for a socket")

ErrSocketPathTooLong is a state root deeper than a unix socket may be named in, and it is the one failure on this road that is settled BEFORE anything is started: no lock, no process, no wait. The caller can therefore name this refusal separately from a host that had somewhere to listen but did not come up.

Functions

func Ask

func Ask(workspace string, ask remote.WhoIs) (remote.HostSelf, error)

Ask puts internal/remote's version exchange to this workspace's host on a connection of its own, and fails when no host answers.

THE CONNECTION IS SPENT ON THE QUESTION AND NEVER BECOMES A SURFACE. A hello would open a conversation — the expensive, journal-locking act this whole exchange exists to keep from happening against the wrong build — so the question travels alone and the caller dials again for the real thing.

func Attach

func Attach(workspace string, spawn func() error) (net.Conn, error)

Attach is the door `codeaf engine` knocks on: a connection to this workspace's host, starting one if nothing answers.

THE SPAWN RACE IS GUARDED BY THE LOCK THE HOST ITSELF HOLDS, which is the discipline this tree already uses for anything one machine may only do once (internal/filelock, and the standing tick's own lock). Whoever takes the lock spawns; whoever finds it busy knows a host is alive or being born and simply waits for the socket. Two spawns that race are not a fault either — the second host to start finds the lock held and exits without a word — so the worst case here is one wasted process, never two hosts on one workspace.

Every failure answers the same way: no connection and a reason, which the caller reads as "serve this one on the pipe".

func Dial

func Dial(workspace string) (net.Conn, error)

Dial connects to a host that is already running, and fails when none is. It never starts one — see Attach for that.

func Dir

func Dir(workspace string) (string, error)

Dir is where the host for one workspace keeps its socket: a directory under ~/.codeaf/v3/hosts, resolved through internal/home so CODEAF_HOME moves it with everything else.

THE DIRECTORY IS NAMED BY A HASH AND NOT BY THE PATH, which is the one place this parts from the session layout's own encoding (chatv3_layout.go turns separators into dashes and keeps the name readable). A socket path has a hard ceiling of about a hundred bytes, and a workspace six directories deep would spend all of it — so the name is short by construction and the readable answer is written INSIDE the directory instead ([placeName]).

func Held

func Held() ([]string, error)

Retire asks this workspace's host to go, and does not return until it has.

IT IS THE HOST THAT SAYS WHETHER IT MAY. A host holding a turn, a surface or an unanswered question answers ErrHostBusy and stays where it is; anyway asks for it regardless, which is one person's own `codeaf engine --stop` and nothing else. Either way the ending is the host's own shutdown — every conversation closed, every journal flushed — and never a signal from outside. Held is every workspace this machine has a host directory for, in the plain text each host wrote there ([placeName]).

IT READS THE DIRECTORIES AND NOT THE PROCESS TABLE. A host is known by the state it left under ~/.codeaf/v3/hosts, so this answers for hosts started by any build and by any terminal, including one whose process has gone and left its socket behind. Whether anybody is actually listening is the caller's next question, asked through Dial or Stop — and a directory whose host is gone answers "nothing was holding it", which is the truth a sweep wants.

A directory with no workspace file is SKIPPED rather than guessed at: the name is a hash and there is no way back from it to a path, so the honest answer for one is nothing at all. Nothing here is an error a caller should stop for — a state root that cannot be read is a machine with no hosts.

func Retire

func Retire(workspace string, anyway bool) error

func Run

func Run(workspace string, opts Options) error

Run becomes this workspace's host and does not return until it is done: the idle policy retires it, a signal asks it to stop, or the listener breaks.

It answers ErrHostRunning when another host already holds this workspace, which the caller treats as success — somebody else did the job.

func SocketPath

func SocketPath(workspace string) (string, error)

SocketPath is the socket one workspace's host listens on, refused up front when the path is longer than a unix socket may be. It makes nothing — see [where] — so a caller that is going to listen on it takes Dir as well.

func SocketPathFits

func SocketPathFits(path string) bool

SocketPathFits exposes the shared Unix-socket ceiling to other doors that place a socket under the codeaf state root. Keeping the number here prevents ssh control sockets and engine-host sockets from drifting across platforms.

func Spawn

func Spawn(workspace, name string, args ...string) error

Spawn starts a host process and lets go of it.

IT IS DETACHED ON PURPOSE AND THAT IS THE POINT OF THE WHOLE LANE. The process asking for it is `codeaf engine` under sshd, and when the connection drops sshd takes down everything in that session's process group — which is exactly the death this package exists to survive. So the child gets a session of its own ([detach]) and none of the parent's input or output: its stdout is the one thing that must never carry a stray byte, because a host's stdout is nothing at all and its parent's is the protocol.

STDERR IS THE ONE PIPE THAT LEAVES LAST WORDS BEHIND. A host that dies at birth is the one process that knew the reason, and sending that reason to /dev/null made the failure unknowable; it appends to the same host log that a running Host writes, with /dev/null as the best-effort fallback when that log cannot be opened.

func Splice

func Splice(in io.Reader, out io.Writer, conn net.Conn) error

Splice is `codeaf engine` once it has a host: everything the surface says goes to the socket, everything the host says goes back, and nothing in between is read.

THE PROXY UNDERSTANDS NO FRAMES, and that is deliberate. The handshake, the resume cursors, the held questions and the stream sequence numbers are all between the surface and the engine; a splice that parsed them would be a third opinion about a protocol with two ends. It copies bytes.

EITHER SIDE ENDING ENDS BOTH. Stdin closing is the ssh connection going away, and the host must see that as the torn pipe it is — so the socket is closed rather than left half-open, and the host reads it as a surface that will be back (internal/remote's server.leave states what it does with that).

func Stop

func Stop(workspace string) (bool, error)

Stop is `codeaf engine --stop`: whatever is holding this workspace on this machine, stopped, whichever build it is.

IT HAS TO WORK ON A BUILD THAT PREDATES THE EXCHANGE, because that is the only build a person ever needs it for — the stale half in the middle, still answering a socket after the binary under it was replaced. So a host that cannot be asked anything is ended the one way the operating system offers: the kernel names the process on the other end of the socket ([peerPID]) and it is sent the signal every build of the host has always answered by flushing its journals and exiting.

It answers false when nothing was holding this workspace, which is not a failure and is the ordinary case.

Types

type Host

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

Host is one workspace's conversations, and the socket they are reached through.

type Options

type Options struct {
	// Boot opens one conversation for a hello — cmd/codeaf's bootEngine, the
	// same closure the pipe engine hands to [remote.Serve].
	//
	// It is called with the host's own lock held, so ONE CONVERSATION IS OPENED
	// AT A TIME. That is not for speed, it is for the session file: two
	// connections asking for the same conversation at the same moment must not
	// both open it, because the second would find the journal locked and mint a
	// second session under a person who asked for one.
	Boot func(remote.Hello) (*remote.Engine, error)

	// Key says which conversation a hello wants, so that two surfaces asking
	// for the same one are handed the same one.
	//
	// IT ANSWERS A TRANSCRIPT PATH, INCLUDING FOR A HELLO THAT NAMED NOTHING.
	// "The workspace's latest" is a question about this machine's disk, and the
	// door that starts the host is the half of this pair that can read it
	// (cmd/codeaf's [engineHelloKey]) — resolving it here, at the door, is what
	// lets the lookups below find a conversation this host is ALREADY holding
	// rather than booting a second agent onto its journal.
	//
	// A nil Key falls back to the hello's own session, which is what a test with
	// no door behind it means, and the empty string it answers for a hello that
	// named nothing is then the boot's problem rather than an identity.
	Key func(remote.Hello) string
}

Options is what a host needs from the door that starts it, which is the same two things every version of this has needed: how to open a conversation, and which conversation a hello is asking for.

Jump to

Keyboard shortcuts

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