daemon

package
v0.11.0-beta.2 Latest Latest
Warning

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

Go to latest
Published: Oct 23, 2026 License: Apache-2.0, MIT Imports: 15 Imported by: 0

Documentation

Overview

Package daemon implements the job queue for overkill's daemon mode (§8.7.3).

Jobs arrive via the ACP /v1/jobs endpoint and are persisted in PostgreSQL so the queue survives restarts. A Worker pool picks them up, calls a pluggable RunFunc, and transitions the job through the state machine:

queued → running → completed
                 → failed
queued → cancelled   (explicit cancel before pickup)
running → cancelled  (explicit cancel during run)
running → suspended  (waiting for remote approval)

Package daemon — UNIX-socket RPC between the running daemon and CLI or TUI clients. Line-delimited JSON over a stream socket; one request per line, one response per line. Small surface on purpose — anything richer than ping/list/cancel either grows the protocol explicitly or reads/writes the Postgres DB directly (the daemon and clients share the same database).

Why UNIX socket and not gRPC / HTTP?

  • Zero dependencies; net package only
  • Filesystem permissions act as the access boundary (0600 on the socket file → same user only)
  • No port allocation, no localhost binding to fight firewalls
  • Tests use a short private socket directory on macOS

Index

Constants

This section is empty.

Variables

View Source
var ErrDaemonDown = errors.New("daemon: not running (socket not reachable)")

ErrDaemonDown signals "couldn't reach the daemon socket at all" — as opposed to "the daemon reached returned an error". Lets the CLI tell the user "start the daemon first" instead of leaking a socket-not- found error from net.Dial.

Functions

func SocketPath

func SocketPath() (string, error)

SocketPath returns the canonical daemon socket path. Distinct from the pidfile because some platforms (macOS) restrict socket paths to 104 chars; we keep the directory shallow.

Types

type Client

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

Client is a one-shot RPC caller. Each Call opens a fresh connection — the daemon socket is not heavily used and pooling adds complexity (idle conn timeouts, broken-pipe detection) for negligible win.

func NewClient

func NewClient(path string) *Client

NewClient returns a Client bound to the standard daemon socket path or a custom path (tests). Default timeout 5s; raise if a downstream op is known to be slow.

func (*Client) Call

func (c *Client) Call(op string, params any) (json.RawMessage, error)

Call sends op+params and returns the result bytes. ErrDaemonDown is returned when the socket can't be reached at all — useful for the CLI to surface "is the daemon running?" hints.

func (*Client) WithTimeout

func (c *Client) WithTimeout(d time.Duration) *Client

WithTimeout returns a copy with a different per-call deadline.

type Handler

type Handler func(ctx context.Context, req Request) (Response, error)

Handler resolves one request to a response. Returning a non-nil error short-circuits to a Response with the error's Error() as Err and Code "internal".

type Job

type Job struct {
	ID        string    `json:"id"`
	SessionID string    `json:"session_id"`
	Intent    string    `json:"intent"` // the user prompt
	Status    JobStatus `json:"status"`
	Channel   string    `json:"channel"` // originating bridge channel
	ChatKey   string    `json:"chat_key"`
	Profile   string    `json:"profile"` // "remote" for bridge-originated
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
	Error     string    `json:"error,omitempty"`
}

Job is one unit of deferred work submitted via the ACP bridge.

func NewJob

func NewJob(intent, channel, chatKey, profile string) Job

NewJob constructs a Job with a fresh ID and the current timestamp.

type JobStatus

type JobStatus string

JobStatus is the current lifecycle state of a Job.

const (
	JobQueued    JobStatus = "queued"
	JobRunning   JobStatus = "running"
	JobSuspended JobStatus = "suspended" // waiting for remote approval
	JobCompleted JobStatus = "completed"
	JobFailed    JobStatus = "failed"
	JobCancelled JobStatus = "cancelled"
)

type JobStore

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

JobStore persists jobs to PostgreSQL.

func NewJobStore

func NewJobStore(db *sql.DB) (*JobStore, error)

NewJobStore wraps a *sql.DB. Caller owns DB lifecycle.

func (*JobStore) Cancel

func (s *JobStore) Cancel(ctx context.Context, id string) error

Cancel transitions a job to JobCancelled.

func (*JobStore) Close

func (s *JobStore) Close() error

Close is a no-op; the caller owns the DB lifecycle.

func (*JobStore) Create

func (s *JobStore) Create(ctx context.Context, job Job) error

Create persists a new Job. The job must have a non-empty ID; callers should set it with uuid.New().String() before calling Create.

func (*JobStore) Get

func (s *JobStore) Get(ctx context.Context, id string) (*Job, error)

Get retrieves a Job by ID.

func (*JobStore) List

func (s *JobStore) List(ctx context.Context) ([]Job, error)

List returns active jobs sorted by CreatedAt descending (newest first).

func (*JobStore) UpdateStatus

func (s *JobStore) UpdateStatus(ctx context.Context, id string, status JobStatus, errMsg string) error

UpdateStatus transitions a job to status.

type RPCError

type RPCError struct {
	Code    string
	Message string
}

RPCError wraps a daemon-returned error. Code is the machine-readable kind; Message is the human description.

func (*RPCError) Error

func (e *RPCError) Error() string

type Request

type Request struct {
	Op     string          `json:"op"`
	Params json.RawMessage `json:"params,omitempty"`
}

Request is one inbound RPC. Op is required; Params is op-specific.

type Response

type Response struct {
	Result json.RawMessage `json:"result,omitempty"`
	Err    string          `json:"error,omitempty"`
	Code   string          `json:"code,omitempty"`
}

Response is one outbound RPC reply. Either Result or Err is set; never both. Code lets clients branch on machine-readable error kinds without parsing the human Err string.

type RunFunc

type RunFunc func(ctx context.Context, job Job) error

RunFunc is the callback invoked by a Worker for each job it picks up.

type Server

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

Server owns the listener + accept loop. Stop() is idempotent so shutdown paths (signal + explicit) can both call it without racing.

func NewServer

func NewServer(path string) *Server

NewServer returns an unstarted server bound to path. The path is removed first if a stale socket exists from a prior process; we don't try to detect liveness — the caller's pidfile check is the authority for "another daemon is running."

func (*Server) Register

func (s *Server) Register(op string, h Handler)

Register binds op → handler. Last write wins so the daemon's wiring can override the built-in ping if it ever wants to.

func (*Server) Start

func (s *Server) Start() error

Start begins accepting connections. Returns after the listener is bound; the accept loop runs in a goroutine until Stop().

func (*Server) Stop

func (s *Server) Stop()

Stop tears down the listener and waits for in-flight conns. Safe to call multiple times.

type Worker

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

Worker is a bounded pool that picks up queued jobs, runs them, and updates their status in JobStore.

func NewWorker

func NewWorker(store *JobStore, run RunFunc, n int) *Worker

NewWorker creates a Worker with concurrency limited to n parallel jobs.

func (*Worker) Start

func (w *Worker) Start(ctx context.Context)

Start launches the background dispatch loop and recovers any orphaned jobs from a previous daemon crash (queued or running jobs that never finished). Also starts a TTL watcher that periodically resets stuck "running" jobs (M18).

func (*Worker) Stop

func (w *Worker) Stop()

Stop shuts down the worker pool gracefully, waiting for in-flight jobs and the TTL watcher to complete.

func (*Worker) Submit

func (w *Worker) Submit(job Job) error

Submit enqueues a job for execution. When the in-memory queue is full (256 capacity), the job is persisted directly to the store as "queued" so it survives and is picked up on the next recovery cycle (M14).

Jump to

Keyboard shortcuts

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