session

package
v0.1.0 Latest Latest
Warning

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

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

Documentation

Overview

Package session runs one research session end to end.

Everything from creating the session row to finalizing its status: boot recovery, the planner–executor–verifier loop, §11.5.2's grounding pass, and the report paid from released escrow (§8.3).

It exists because two callers need that sequence and only one of them has a terminal. `mole research` had all of it inline, interleaved with flag handling and fmt.Printf, which made the daemon's options "duplicate 240 lines" or "import a CLI". Nothing here prints, opens a database, reads config, or knows what a flag is — callers inject what they built and receive what happened.

The split into Recover / Create / Run is not cosmetic. Recovery is per PROCESS, so a daemon sweeps once at boot rather than once per session. And a daemon must return a session id to its caller before the work finishes, so creating the row and running the loop cannot be one call.

Index

Constants

View Source
const DefaultMaxConcurrent = 4

DefaultMaxConcurrent bounds sessions running at once.

Small on purpose. Each session holds a lead queue, a cache and a model client, and they all contend for one SQLite writer — the ceiling that matters is not CPU. M5's executor pool is where this becomes a real scheduler; until then a low bound that refuses honestly beats a high one that thrashes.

Variables

View Source
var ErrAtCapacity = errors.New("session: at capacity")

ErrAtCapacity is returned by Start when MaxConcurrent sessions are running.

Refusing rather than queueing is deliberate for this milestone. A queued session would hand the caller an id, report "running", and do nothing for an unknown period — a status that lies. M5 adds a real queue with a state to match; until then the honest answer is no.

View Source
var ErrNoSuchSession = errors.New("session: not running")

ErrNoSuchSession is returned for an id the supervisor is not running.

View Source
var ErrShutdown = errors.New("session: supervisor is shut down")

ErrShutdown is returned once the supervisor has been shut down.

Functions

This section is empty.

Types

type Recovery

type Recovery struct {
	Leads        int
	Reservations int
	Sessions     int
	// Documents is stored source text deleted for being past its TTL. Reclaimed
	// disk rather than recovered state, and reported for the same reason: a
	// number nobody prints is a sweep nobody notices has stopped running.
	Documents int64
}

Recovery reports what boot recovery reclaimed.

func (Recovery) Any

func (rc Recovery) Any() bool

Any reports whether anything was recovered.

type Result

type Result struct {
	Session *core.Session

	// Run is the loop's own result. Nil when the loop never started.
	Run *executor.Result
	// Report is the synthesized answer. Nil when generation failed — never fatal,
	// since a failed synthesis costs the prose and not the evidence.
	Report *output.Report
	// Ground is §11.5.2's pass. Nil when it did not run.
	Ground *verifier.GroundReport

	// Status is what the session was finalized as.
	Status core.SessionStatus
	// Err is the loop's error. Returned in the struct rather than as the
	// function's error because a failed loop still has a session, a spend, and
	// usually claims — a caller that gets only an error has to go and find them.
	Err error
}

Result is what happened, in the shape a caller needs to render or return.

type Runner

type Runner struct {
	Store store.Store
	Actor *actors.WebActor

	// VerifierModel overrides the cheap model for adjudication. Empty leaves the
	// cheap model in place; set it when the cheap model cannot tell a
	// contradiction from two unrelated statements.
	VerifierModel string
	// VerifierBatchSize caps pairs per adjudication call. Zero takes the default.
	VerifierBatchSize int

	// NoConfirmEdges disables the second adjudication pass. Phrased as an opt-OUT
	// because confirming is the measured default and the flag exists for somebody
	// who has measured differently on their own data.
	NoConfirmEdges bool

	// Academic researches scholarly leads (§10.2). Nil disables the actor.
	//
	// Nil in two cases, not one: an install with no contact email (§10.3 makes
	// that a hard requirement), and every daemon — `mole serve` does not build
	// it, so the academic path is CLI-only today.
	Academic *actors.AcademicActor

	// Local answers leads from the user's own registered data (§12). Nil
	// disables the actor, which is the state of any install where nobody has
	// run `mole connect add`.
	Local *actors.LocalComputeActor

	// Owner names what is running the loop, for lead leases (§9.4). A daemon and
	// a CLI must not claim each other's leads.
	Owner string

	Log *slog.Logger

	// Progress receives executor events as they happen.
	Progress func(executor.Event)
	// OnLoop fires when the research loop finishes, before any escrow is released.
	// OnGround fires when the grounding pass finishes, before the report is
	// generated.
	//
	// Callbacks rather than fields on Result because the sequence is observable
	// and Result is not: the CLI prints the run summary, then grounding, then the
	// report, in that order. Returning all three at the end collapses that into
	// one moment and the caller has to guess the order — which is exactly what
	// went wrong when this package was first extracted, and the grounding line
	// started printing ahead of the summary it follows.
	//
	// A daemon uses the same two points to publish status transitions without
	// waiting for the report.
	OnLoop   func(*executor.Result)
	OnGround func(*verifier.GroundReport)
	// Notice receives non-fatal problems: a sweep that failed, a hold that could
	// not be released, a report that would not generate. None of these stops a
	// run, and all of them need saying. The CLI writes them to stderr; a daemon
	// logs them.
	Notice func(string)
}

Runner holds the collaborators a session needs.

The store and actor are built by the caller: the CLI from flags and config, the daemon from its own configuration. Neither is constructed here, because choosing a search provider or a database path is not this package's decision.

func (*Runner) Create

func (r *Runner) Create(ctx context.Context, spec Spec) (*core.Session, error)

Create writes the session row and returns it.

Separate from Run so a daemon can hand a session id back to its caller before any research happens. Nothing is spent here beyond the escrow reserved at creation (§8.3).

func (*Runner) PurgeDocuments

func (r *Runner) PurgeDocuments(ctx context.Context) (int64, error)

PurgeDocuments deletes stored source text past its retention TTL and reports how many rows went.

Exported so a long-running daemon can repeat it: boot recovery is not enough for a process that stays up for weeks, which is the process this table fills up fastest under.

func (*Runner) Recover

func (r *Runner) Recover(ctx context.Context) Recovery

Recover reclaims what a previous process left behind (§9.4).

Per PROCESS, not per session — which is why it is not inside Run. It is deliberately unscoped: boot recovery does not know which sessions were in flight, and this is the one caller for which that is correct. A daemon calls it once at startup; running it per session would sweep leases belonging to sessions currently running alongside.

Never returns an error. Every sweep is best-effort recovery of someone else's mess, and refusing to start because a sweep failed helps nobody.

func (*Runner) Run

func (r *Runner) Run(ctx context.Context, sess *core.Session, spec Spec) (*Result, error)

Run drives the loop for an already-created session, then writes the report and finalizes the status.

The caller owns the context. Give it headroom over spec.Timeout: when the context deadline and the wall-clock ceiling fire together the loop is killed mid-lead instead of stopping at its own check, and the run ends in a cascade of "context deadline exceeded" from whatever was in flight — a persist, a settle, a count — rather than a clean "stopped: max_wallclock".

A loop error is returned inside Result, not as the error, so a caller still gets the session, the spend and the claims. The error return is for failures that leave nothing worth reporting.

type Spec

type Spec struct {
	Question   string
	Mode       core.Mode
	BudgetUnit core.BudgetUnit
	Budget     int64

	// MaxSources is sources read per lead; MaxDepth is rounds of follow-up leads
	// the planner may add; MaxLeads bounds the whole session (§8.5).
	MaxSources int
	MaxDepth   int
	MaxLeads   int

	// Timeout is the session's wall-clock ceiling (§8.5). The caller owns the
	// context deadline; this is the ceiling the loop checks itself against, and
	// the two should not be equal — see Runner.Run.
	Timeout time.Duration

	// Schema is the dataset schema, required when Mode is ModeDataset and unused
	// otherwise (M9, §13).
	Schema *dataset.Schema

	// ActorTypes are the actors this session may use. Empty means web only.
	//
	// Session-level rather than planner-chosen: the actor has to be provably
	// working before the planner starts making choices about it, and a session
	// that never asked for academic sources should never pay for one.
	ActorTypes []core.ActorType

	// Workers is how many leads this session runs at once. Zero takes
	// executor.DefaultWorkers. Not persisted on the session row: it is a
	// property of the process doing the work, not of the research, and a
	// recovered session is re-run by whatever is configured then.
	Workers int
}

Spec is one research request: what to ask and what it may spend.

Separate from Runner because a Runner's collaborators outlive any single session, while these values arrive with the request.

type Supervisor

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

Supervisor runs sessions in the background.

The daemon needs this because MCP is asynchronous: research.report returns a session id immediately and the work continues long after the call — and long after the calling agent's own context is gone. So a session's lifetime cannot be tied to the request that started it, which is the single most important thing this type gets right. Start uses the caller's context only to create the row; the run itself hangs off a context the supervisor owns.

func NewSupervisor

func NewSupervisor(r *Runner, max int, log *slog.Logger) *Supervisor

NewSupervisor returns a supervisor that runs at most max sessions at once.

func (*Supervisor) Cancel

func (s *Supervisor) Cancel(id string) error

Cancel stops a running session.

Returns once the session has been asked to stop, not once it has. Callers that need to know it finished should Wait. The distinction matters: a lead mid-fetch takes as long as the fetch does, and an MCP caller should not be blocked on it.

func (*Supervisor) Running

func (s *Supervisor) Running() []string

Running lists the session ids currently in flight, sorted.

func (*Supervisor) Shutdown

func (s *Supervisor) Shutdown(ctx context.Context) error

Shutdown cancels every running session and waits for them to finish.

Waits rather than returning immediately, because the thing being waited for is each session releasing its budget holds. A daemon that exits while a reservation is open leaves money neither spent nor available until the next boot's sweep reclaims it (§9.4) — recoverable, but only later, and only by somebody who runs mole again.

If ctx expires first the sessions are still cancelled and the error says so; their holds are then left to that sweep.

func (*Supervisor) Start

func (s *Supervisor) Start(ctx context.Context, spec Spec) (*core.Session, error)

Start creates a session and begins running it in the background.

The returned session exists and has its budget reserved before this returns, so a caller can hand the id back immediately and poll. ctx bounds only the creation: the run continues after it is cancelled, which is the whole point.

func (*Supervisor) Wait

func (s *Supervisor) Wait(ctx context.Context, id string) (*Result, error)

Wait blocks until the session finishes, or ctx is done.

An id the supervisor is not running is an error rather than an immediate return: "already finished" and "never existed" are different answers, and only the store can tell them apart.

Jump to

Keyboard shortcuts

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