state

package
v0.14.0 Latest Latest
Warning

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

Go to latest
Published: Aug 30, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

Documentation

Overview

Package state persists what the server knows across process lifetimes.

The task registry used to live only in RAM. That was fine as long as there was exactly one server process, but MCP clients do start a second instance — sometimes alongside the first rather than in place of it. The new process then came up blank: agent_list_tasks returned nothing and every task_id from before was unknown, while the workers those tasks owned kept running happily under the original process, still writing their results to disk. Nothing was broken except the server's ability to see its own work.

This package closes that gap from both ends. It writes each task to disk as it progresses, so a later instance can list and read what came before. And it keeps a PID lock, so an instance can tell that it is not the only one running instead of silently presenting an empty registry as the truth.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DefaultDir

func DefaultDir() string

DefaultDir is where state goes when the operator names no directory: %AppData%\cli-agent-mcp on Windows, ~/.config/cli-agent-mcp elsewhere.

func ResolveDir added in v0.13.0

func ResolveDir(dir string) string

ResolveDir turns a configured directory into the absolute path actually used, applying the default for an empty value. Callers that need to read state without opening a store — the pairing check runs before anything else, and must not create directories on behalf of a launcher it has yet to authorize — go through this.

Types

type Follower

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

Follower streams a task's transcript as it grows, returning only what is new since the last call. It is the primitive behind `cli-agent-mcp logs -f` and the local web viewer.

It re-opens the file on every poll rather than holding a handle. At this cadence that costs nothing, and it keeps a reader from being one more handle contending with the writer's own append handle on Windows.

func (*Follower) Next

func (f *Follower) Next() ([]string, error)

Next returns the transcript lines that appeared since the previous call, in order. It never blocks: no new output yields no lines, which is what lets a caller poll it on its own schedule.

type Owner

type Owner struct {
	PID     int       `json:"pid"`
	Started time.Time `json:"started"`
	Exe     string    `json:"exe"`
}

Owner identifies the process that holds the lock.

type Store

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

Store is the on-disk home for task records and the instance lock. Its methods are safe for concurrent use.

func Open

func Open(dir string) (*Store, error)

Open prepares dir for use, creating it if needed.

func (*Store) Acquire

func (s *Store) Acquire() (previous *Owner, err error)

Acquire records this process as the owner of the state directory and reports the previous owner when that process is still alive.

It deliberately does not refuse to start on a conflict. The client that just launched this process is talking to it and nothing else; failing here would leave the user with no server at all, which is worse than having two. The caller's job is to surface the conflict, not to prevent it.

func (*Store) AppendLine

func (s *Store) AppendLine(id, line string) error

AppendLine adds one transcript line to the task's log. Lines are written as they arrive so another instance can read a run that is still in progress.

func (*Store) CancelRequested added in v0.11.0

func (s *Store) CancelRequested(id string) bool

CancelRequested reports whether a stop has been asked for.

func (*Store) ClearCancel added in v0.11.0

func (s *Store) ClearCancel(id string) error

ClearCancel drops the request once it has been acted on, so a task id reused by a later run does not inherit it.

func (*Store) Close

func (s *Store) Close()

Close releases the open log handles. It leaves the lock file in place: a stale lock is harmless because Acquire checks whether the recorded process is still alive, whereas deleting it on the way out would erase the evidence in exactly the case that matters — a process that was killed rather than shut down cleanly.

func (*Store) CountTasks added in v0.13.0

func (s *Store) CountTasks() int

LoadTasks returns every stored record, oldest file first. Records that fail to parse are skipped rather than failing the whole load: one corrupt file must not cost the operator the rest of their history. CountTasks reports how many task records are on disk, without reading them.

It exists for the one line that has to say what is NOT being loaded: when another server instance is live, this one leaves its records alone (issue #21), and "isolated: 4 task record(s) belong to pid 1234" is the difference between a startup log that explains an empty listing and one that lets the user think their tasks vanished.

func (*Store) Dir

func (s *Store) Dir() string

Dir reports the directory in use, for logging and diagnostics.

func (*Store) Follow

func (s *Store) Follow(id string, lastN int) (*Follower, error)

Follow opens a follower over a task's transcript.

lastN >= 0 positions it so the first Next returns the final lastN lines already on disk (0 means "only what arrives from now on"); lastN < 0 starts at the beginning of the transcript. A task that has not written anything yet is not an error — the follower simply yields nothing until it does.

func (*Store) Forget

func (s *Store) Forget(id string)

Forget drops a task's files, for when the manager evicts it from memory.

func (*Store) LoadTask

func (s *Store) LoadTask(id string) (json.RawMessage, error)

LoadTask returns one stored record, or nil when there is none. It is how a task owned by another process gets re-read: that process keeps rewriting the record, so this is the only way to learn that it finished.

func (*Store) LoadTasks

func (s *Store) LoadTasks() ([]json.RawMessage, error)

func (*Store) Owner

func (s *Store) Owner() *Owner

Owner reports the process recorded in the lock file, when that process is still alive.

It exists because Acquire cannot be used for this: Acquire *writes* the lock, so a read-only viewer calling it would take ownership away from the running server and leave the next real instance believing it was alone. Reading is the whole contract here.

func (*Store) Prune

func (s *Store) Prune(keep int)

Prune keeps the newest `keep` task records and deletes the rest. Without it the directory would grow for the life of the installation.

func (*Store) ReadLines

func (s *Store) ReadLines(id string) ([]string, error)

ReadLines returns a task's stored transcript.

func (*Store) RequestCancel added in v0.11.0

func (s *Store) RequestCancel(id string) error

RequestCancel records that someone wants a task stopped.

It exists because the process that can actually stop a worker is the one that spawned it, and that is not always the process being asked. Clients do start a second server instance alongside the first, and from there a task belonging to the other one is visible — its record and transcript are on disk and keep advancing — but untouchable.

Killing the worker by pid from outside was the obvious alternative and is not safe: pids are recycled, briskly on Windows, so a stale record would eventually name a process that has nothing to do with us. Leaving the request where the owner will find it costs a moment's delay and cannot kill the wrong thing.

func (*Store) SaveTask

func (s *Store) SaveTask(id string, v any) error

SaveTask writes v as the task's record, replacing any previous one.

The write goes to a temp file and is renamed into place, so a process that dies mid-write leaves the previous record intact rather than a truncated one that would fail to parse on the next startup.

Jump to

Keyboard shortcuts

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