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 ¶
- Variables
- type Config
- type PassResult
- type RequestResult
- type RequestScope
- type Service
- func (s *Service) Apply(ctx context.Context, id string) (map[string]any, error)
- func (s *Service) CanRequest() bool
- func (s *Service) Dismiss(ctx context.Context, id string) error
- func (s *Service) Request(ctx context.Context, text string, scope RequestScope) (RequestResult, error)
- func (s *Service) RunOnce(ctx context.Context) (PassResult, error)
- func (s *Service) Split(ctx context.Context, source, instruction string) (RequestResult, error)
- func (s *Service) Start(ctx context.Context)
- func (s *Service) Wait()
Constants ¶
This section is empty.
Variables ¶
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.