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 ¶
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 ¶
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.
var ErrNoSuchSession = errors.New("session: not running")
ErrNoSuchSession is returned for an id the supervisor is not running.
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.