gardener

package
v0.4.5 Latest Latest
Warning

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

Go to latest
Published: Jul 26, 2026 License: MIT Imports: 24 Imported by: 0

Documentation

Overview

Package gardener runs the propose-only maintenance passes over the memory corpus: it finds near-duplicate memories (dedup), memories that have gone untouched for too long (staleness), stage memories that stopped carrying a live gate (stale-stage), memories briefings keep injecting without any demand (dead-weight), captured plans that were never approved (stale-plan), recall queries that keep missing because nobody wrote the memory (memory-wanted), errors agents keep hitting on tool calls or hook stages (tool-error), and rolls recent sessions into a monthly digest. Every pass only ever writes a gardener_proposals row for the owner to apply or dismiss -- it never mutates a memory on its own. The passes run on a ticker and are also invokable on demand (RunOnce).

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrNoChat       = errors.New("gardener: natural-language requests need an LLM chat client")
	ErrEmptyRequest = errors.New("gardener: empty request")
	ErrUnparseable  = errors.New("gardener: could not parse the interpreter's output")
)

Sentinel errors for Request, checked with errors.Is.

View Source
var ErrNoSource = errors.New("gardener: split needs a source project")

ErrNoSource is returned by Split when no source project is given.

Functions

This section is empty.

Types

type Config

type Config struct {
	DedupThreshold float64
	StalenessDays  int
	DigestDays     int
	Interval       time.Duration
	// ToolEventRetentionDays prunes transport-level Interactions events
	// (tool.call, hook.prompt, recall.miss, hook.error) older than this many
	// days on each pass. 0 disables pruning -- deliberately not defaulted,
	// since 0 means "keep forever".
	ToolEventRetentionDays int
	// StalePlanDays proposes abandoning captured Claude Code plans still in
	// draft/presented after this many days. 0 disables the pass -- deliberately
	// not defaulted, mirroring ToolEventRetentionDays.
	StalePlanDays int
	// StaleStageDays proposes archiving stage memories whose Status header is
	// not a live gate (done, missing, or unrecognized) after this many days
	// without an update. 0 disables the pass -- same contract as StalePlanDays.
	StaleStageDays int
	// SessionIdle is the no-activity age past which the reaper expires an
	// active session. Non-positive falls back to core.SessionIdleTTL, keeping
	// the reaper cutoff aligned with the console's live/idle derivation.
	SessionIdle time.Duration
}

Config parameterizes the gardener passes.

func FromConfig

func FromConfig(g config.Gardener) Config

FromConfig adapts the server config's gardener block to the pass Config.

type PassResult

type PassResult struct {
	Merges       int `json:"merges"`
	Archives     int `json:"archives"`
	Digests      int `json:"digests"`
	StalePlans   int `json:"stalePlans"`
	MemoryWanted int `json:"memoryWanted"`
	ToolError    int `json:"toolError"`
	// Failed names the passes that errored, in run order. A failing pass leaves
	// its count at 0, which is shaped exactly like "nothing to propose"; this is
	// the only thing that tells the two apart. Read it before trusting a zero.
	Failed []string `json:"failed,omitempty"`
}

PassResult counts what a single RunOnce produced.

func (PassResult) OK

func (r PassResult) OK() bool

OK reports whether every pass ran to completion. A zero Total means "nothing to propose" only when OK is true.

func (PassResult) Total

func (r PassResult) Total() int

Total is the number of proposals created across all passes.

type RequestResult

type RequestResult struct {
	Created []string       `json:"created"` // proposal ids, in creation order
	ByKind  map[string]int `json:"byKind"`  // e.g. {"merge":2,"archive":1}
	Total   int            `json:"total"`
	Skipped []string       `json:"skipped"` // human-readable notes on dropped ops
	Summary string         `json:"summary"` // one-line summary

	// Guidance is a human/agent-readable note on how to proceed when the request
	// is better served by another tool -- most importantly when it recognizes a
	// project split and points the caller at gardener_split. Empty when the
	// request was handled inline.
	Guidance string `json:"guidance,omitempty"`
	// SplitIntent is true when the request was recognized as a project split,
	// whether or not a source project was identified. It lets a caller offer its
	// own follow-up (the console shows a project picker) without parsing Guidance.
	SplitIntent bool `json:"splitIntent,omitempty"`
	// SplitSource is set to the source project slug when the request was
	// recognized as a project split that should be planned with gardener_split.
	// The general request never plans a split itself (it needs a whole-project
	// classification and a known source); it only routes to it.
	SplitSource string `json:"splitSource,omitempty"`
}

RequestResult summarizes one interpretation pass.

type RequestScope

type RequestScope struct {
	// Project scopes to one project's memories plus the globals. The zero value
	// -- "" -- is globals only, matching store.ActiveMemories.
	Project string
	// AllProjects widens to every project, ignoring Project. It is only ever set
	// from an explicit caller intent (gardener_request's project="all", the
	// console's "All projects" option), never as a default: an unscoped request
	// that silently read every project is what the input-boundary work exists to
	// remove.
	AllProjects bool
}

RequestScope selects the active memories an interpretation may reference.

It is a struct rather than a scope string because that string was overloaded: "" used to mean "every project on the machine", the inverse of every other scope in the codebase, where an empty project slug IS the global scope. That inversion made the two callers unable to route their project argument through the shared scope guards -- normalizeProject maps "global" to "", which would have turned a globals-only request into a whole-machine scan. Splitting the meanings apart is what lets the boundary own its tokens and this package own none.

type Service

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

Service runs the gardener passes over one store.

func New

func New(db *sql.DB, mgr *files.Manager, embedder llm.Embedder, chat llm.Chat, rec *events.Recorder, cfg Config, logger *slog.Logger) *Service

New builds a gardener Service. embedder and chat may each be nil, disabling the pass that needs them (dedup and digest respectively); staleness always runs. events may be nil (proposal telemetry is then skipped).

func (*Service) Apply

func (s *Service) Apply(ctx context.Context, id string) (map[string]any, error)

Apply carries out a pending proposal and marks it applied. The effect depends on the kind: an archive retires the memory (invalid, but still readable), a merge supersedes the "drop" memory by the "keep" memory, a digest writes the summary as a note, a memory_wanted opens a task to write the missing knowledge, and a rekind reclassifies the memory's kind in place. If the effect cannot be carried out (e.g. a referenced memory has since been deleted), the proposal is left pending and an error is returned, so the owner can retry or dismiss.

func (*Service) CanRequest

func (s *Service) CanRequest() bool

CanRequest reports whether natural-language requests are available (a chat client is configured). The console and MCP gate their UI on it without needing to see the private chat field.

func (*Service) Dismiss

func (s *Service) Dismiss(ctx context.Context, id string) error

Dismiss marks a pending proposal dismissed without any side effect. Its key stays known, so the gardener will not raise the same suggestion again.

func (*Service) Request

func (s *Service) Request(ctx context.Context, text string, scope RequestScope) (RequestResult, error)

Request interprets a single natural-language maintenance request against the active memories in scope and creates PENDING proposals for review. It never mutates a memory.

func (*Service) RunOnce

func (s *Service) RunOnce(ctx context.Context) (PassResult, error)

RunOnce refreshes retrieval stats, then runs the propose-only passes and returns what each created. A pass that fails is logged and skipped; a single failing pass never aborts the others.

func (*Service) Split

func (s *Service) Split(ctx context.Context, source, instruction string) (RequestResult, error)

Split interprets a natural-language request to split source into child projects (with cross-platform memories kept in a shared parent) and creates the PENDING proposals for review: one `split` setup proposal plus one `reproject` per memory, all under plan "split-<source>". It never creates a project or moves a memory -- the owner applies each proposal. instruction is the free-text ask (which children, what stays shared); an empty instruction still works (the model infers reasonable children from the memories), but a source project is required.

func (*Service) Start

func (s *Service) Start(ctx context.Context)

Start launches the gardener ticker in a background goroutine: one pass shortly after startup, then every Interval, until ctx is cancelled. It never blocks the caller and never panics the server (each pass is best-effort). Call it only when the gardener is enabled, at most once, from the goroutine that will later call Wait (the daemon's serve path).

func (*Service) Wait

func (s *Service) Wait()

Wait blocks until the goroutine launched by Start has exited (its ctx must already be cancelled, or Wait blocks until it is). It returns immediately when Start was never called. The daemon calls it during shutdown so no pass is still touching the DB when the DB closes.

Jump to

Keyboard shortcuts

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