diff --git a/internal/config/background.go b/internal/config/background.go new file mode 100644 index 0000000..5a3c387 --- /dev/null +++ b/internal/config/background.go @@ -0,0 +1,69 @@ +package config + +import "time" + +// Two loops that run behind the conversation and never speak. Both are absent +// by default, and both follow the same rule as every other cost the owner did +// not ask for: present-but-empty (`{}`) is a valid "on with the defaults". + +// MemoryEvalConfig — the background memory-evaluation loop (Vikunja #248). +// Absent ⇒ off, like every other capability that costs something the owner did +// not ask for. Each evaluation is a full LLM round-trip on the one resident +// model, which is the same model answering him; running it hourly by default +// would put a multi-second stall in front of an occasional voice turn for a +// feature he may not want. +// +// The loop only ever writes notes (source infer:memory-eval, visible on +// /dash). It cannot speak — see internal/memeval. +type MemoryEvalConfig struct { + // Interval — how often to evaluate. 0 ⇒ DefaultMemoryEvalInterval. + Interval Duration `json:"interval,omitempty"` + + // MaxItems — recent facts / notes / nudges fed into one evaluation. + // 0 ⇒ memeval.DefaultMaxItems. + MaxItems int `json:"max_items,omitempty"` + + // MinConfidence — observations the model scores below this are dropped. + // 0 ⇒ memeval.DefaultMinConfidence. + MinConfidence float64 `json:"min_confidence,omitempty"` +} + +// DefaultMemoryEvalInterval — the plan's cadence (1h) for the memory +// evaluation loop, applied only when the block is present at all. +const DefaultMemoryEvalInterval = time.Hour + +// normaliseMemoryEval leaves an absent block nil (⇒ no evaluation loop) and +// gives a present one the plan's cadence. +func (c *Config) normaliseMemoryEval() { + if c.MemoryEval != nil && c.MemoryEval.Interval <= 0 { + c.MemoryEval.Interval = Duration(DefaultMemoryEvalInterval) + } +} + +// EmailConfig — core's half of the email reader: how many task candidates one +// message may produce, and how long the extraction call may take. +// +// There is deliberately nothing about a mailbox here. Core does not connect to +// IMAP, does not know an account exists, and holds no mail credential — the +// reader daemon does, the same split mavpoll uses for the zenmoney token. This +// block only says "extraction is allowed, with these bounds". +type EmailConfig struct { + // MaxTasks — candidates per message. 0 ⇒ email.MaxCandidates (3). + MaxTasks int `json:"max_tasks,omitempty"` + + // Timeout — per-message extraction budget. 0 ⇒ DefaultEmailTimeout. This is + // a Thinking model reading a mail; nobody is waiting on the answer, but a + // hung llama-server must not pin the reader's connection forever. + Timeout Duration `json:"timeout,omitempty"` +} + +// DefaultEmailTimeout — extraction budget per message. +const DefaultEmailTimeout = 2 * time.Minute + +// normaliseEmail leaves an absent block nil (⇒ mail ingestion refused) and +// gives a present one the timeout default. +func (c *Config) normaliseEmail() { + if c.Email != nil && c.Email.Timeout <= 0 { + c.Email.Timeout = Duration(DefaultEmailTimeout) + } +} diff --git a/internal/config/config.go b/internal/config/config.go index f9e1956..1179e54 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -279,48 +279,6 @@ type Config struct { NetScan *NetScanConfig `json:"netscan,omitempty"` } -// MemoryEvalConfig — the background memory-evaluation loop (Vikunja #248). -// Absent ⇒ off, like every other capability that costs something the owner did -// not ask for. Each evaluation is a full LLM round-trip on the one resident -// model, which is the same model answering him; running it hourly by default -// would put a multi-second stall in front of an occasional voice turn for a -// feature he may not want. -// -// The loop only ever writes notes (source infer:memory-eval, visible on -// /dash). It cannot speak — see internal/memeval. -type MemoryEvalConfig struct { - // Interval — how often to evaluate. 0 ⇒ DefaultMemoryEvalInterval. - Interval Duration `json:"interval,omitempty"` - - // MaxItems — recent facts / notes / nudges fed into one evaluation. - // 0 ⇒ memeval.DefaultMaxItems. - MaxItems int `json:"max_items,omitempty"` - - // MinConfidence — observations the model scores below this are dropped. - // 0 ⇒ memeval.DefaultMinConfidence. - MinConfidence float64 `json:"min_confidence,omitempty"` -} - -// EmailConfig — core's half of the email reader: how many task candidates one -// message may produce, and how long the extraction call may take. -// -// There is deliberately nothing about a mailbox here. Core does not connect to -// IMAP, does not know an account exists, and holds no mail credential — the -// reader daemon does, the same split mavpoll uses for the zenmoney token. This -// block only says "extraction is allowed, with these bounds". -type EmailConfig struct { - // MaxTasks — candidates per message. 0 ⇒ email.MaxCandidates (3). - MaxTasks int `json:"max_tasks,omitempty"` - - // Timeout — per-message extraction budget. 0 ⇒ DefaultEmailTimeout. This is - // a Thinking model reading a mail; nobody is waiting on the answer, but a - // hung llama-server must not pin the reader's connection forever. - Timeout Duration `json:"timeout,omitempty"` -} - -// DefaultEmailTimeout — extraction budget per message. -const DefaultEmailTimeout = 2 * time.Minute - // Duration — a time.Duration that round-trips through JSON as a string // ("60s", "5m", "1h30m"). Plain time.Duration marshals as a nanosecond int, // which is unreadable in a config file; this wrapper uses ParseDuration. @@ -343,7 +301,9 @@ func (d *Duration) UnmarshalJSON(b []byte) error { return nil } -// Defaults applied when the corresponding field is empty/zero. +// The core daemon's own defaults, applied when the field is empty or zero. +// Every other block keeps its defaults in its own file, next to the struct and +// the normalise that applies them. const ( DefaultTickInterval = 60 * time.Second DefaultRepeatInterval = 5 * time.Minute @@ -354,10 +314,6 @@ const ( DefaultIntakeJournal = 512 DefaultFactEnrichmentInterval = 30 * time.Second - - // DefaultMemoryEvalInterval — the plan's cadence (1h) for the memory - // evaluation loop, applied only when the block is present at all. - DefaultMemoryEvalInterval = time.Hour ) // Load reads the JSON config at path and applies defaults. A missing file is @@ -423,17 +379,8 @@ func (c *Config) applyDefaults() { c.normaliseDigest() c.normalisePatternProposals() - // Same rule: absent stays nil (⇒ no evaluation loop), present gets defaults - // so `{}` is a valid "on with the plan's cadence". - if c.MemoryEval != nil && c.MemoryEval.Interval <= 0 { - c.MemoryEval.Interval = Duration(DefaultMemoryEvalInterval) - } - - // Same rule again: absent stays nil (⇒ mail ingestion refused), present gets - // the timeout default so `{}` is a valid "on with the defaults". - if c.Email != nil && c.Email.Timeout <= 0 { - c.Email.Timeout = Duration(DefaultEmailTimeout) - } + c.normaliseMemoryEval() + c.normaliseEmail() c.normaliseFeeds()