71a9a59403
SeedEvent writes the fact at the caller's timestamp, extracts an event from it, and runs the same detectAndPropose the voice path runs. What a seed proves is therefore the daemon's own wiring, not the detector in isolation — which is what an eval-lab fixture would have proved, and is not what the four blocked tasks doubt. The flag is the real lock, not the authority rung. -allow-seed defaults off, and off means daemonAPI.seedStore is nil: the method has nothing to write with rather than permission to refuse. A box that can rewrite its own past says so in its boot log. Seeded facts carry source "seed:qa" and no Subject, so they never queue a Nexus resolution and stay identifiable for the wipe in V-494. Nothing else in the tree writes that source. Best-effort is not the shape here, unlike detectPattern: a seed that half worked is a QA result nobody can trust, so every step reports its own failure. Extraction declining is not a failure, and Extracted says so. Tests cover all four: refused with no flag, four spaced seeds propose and three do not, a value outside the lexicon writes the fact and claims no event, a zero timestamp is refused rather than defaulted to now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011x5DgnExQ5XZy8TZPs5bot
118 lines
3.7 KiB
Go
118 lines
3.7 KiB
Go
// mavend/tick_api.go — the daemonAPI read surface over the tick loop.
|
|
//
|
|
// Split out of tick.go, move-only (Vikunja #422). What mavweb asks the daemon
|
|
// for, and the loop-to-ipc conversions those answers need.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"github.com/kami/maven/internal/ipc"
|
|
"github.com/kami/maven/internal/loop"
|
|
"github.com/kami/maven/internal/store"
|
|
)
|
|
|
|
// daemonAPI wraps a store-backed CoreAPI and overrides TickTrace with the
|
|
// daemon's in-memory tick trace cache.
|
|
type daemonAPI struct {
|
|
ipc.CoreAPI
|
|
getTrace func() *loop.TickTrace
|
|
getMorningStatus func(ctx context.Context) []ipc.MorningRoutineStatus
|
|
getDayPlan func(ctx context.Context) ipc.DayPlan
|
|
chatFn func(ctx context.Context, conversation, text string) string
|
|
getMCPServers func() []ipc.MCPServerStatus
|
|
getEvents func(n int) []ipc.IntakeEvent
|
|
// seedStore — non-nil ONLY when mavend was started with -allow-seed. It is
|
|
// the whole off-switch for the backdated write path (Vikunja #518), and it
|
|
// is a store rather than a bool so that leaving the flag off means the
|
|
// method has nothing to write with, not merely permission to refuse.
|
|
seedStore *store.Store
|
|
}
|
|
|
|
// RecentEvents — the unified intake journal (Vikunja #283). Empty, not an
|
|
// error, when no bus was wired: "nothing has arrived" and "the journal is off"
|
|
// look the same to a reader on purpose, because neither is a fault and the
|
|
// page renders both as an empty table.
|
|
func (d *daemonAPI) RecentEvents(ctx context.Context, n int) ([]ipc.IntakeEvent, error) {
|
|
if d.getEvents == nil {
|
|
return nil, nil
|
|
}
|
|
return d.getEvents(n), nil
|
|
}
|
|
|
|
func (d *daemonAPI) Chat(ctx context.Context, conversation, text string) (string, error) {
|
|
if d.chatFn == nil {
|
|
return "", errors.New("mavend: chat not available")
|
|
}
|
|
return d.chatFn(ctx, conversation, text), nil
|
|
}
|
|
|
|
// MCPServers — the configured MCP servers and their health (Vikunja #251).
|
|
// Empty, not an error, when the mcp block is absent: "not configured" is the
|
|
// default state and the web surface renders it as such.
|
|
func (d *daemonAPI) MCPServers(ctx context.Context) ([]ipc.MCPServerStatus, error) {
|
|
if d.getMCPServers == nil {
|
|
return nil, nil
|
|
}
|
|
return d.getMCPServers(), nil
|
|
}
|
|
|
|
func (d *daemonAPI) TickTrace(ctx context.Context) (ipc.TickTrace, error) {
|
|
trace := d.getTrace()
|
|
if trace == nil {
|
|
return ipc.TickTrace{}, nil
|
|
}
|
|
return toIPCTickTrace(*trace), nil
|
|
}
|
|
|
|
func (d *daemonAPI) MorningStatus(ctx context.Context) ([]ipc.MorningRoutineStatus, error) {
|
|
if d.getMorningStatus == nil {
|
|
return nil, errors.New("mavend: morning status not available")
|
|
}
|
|
return d.getMorningStatus(ctx), nil
|
|
}
|
|
|
|
func (d *daemonAPI) DayPlan(ctx context.Context) (ipc.DayPlan, error) {
|
|
if d.getDayPlan == nil {
|
|
return ipc.DayPlan{}, errors.New("mavend: day plan not available")
|
|
}
|
|
return d.getDayPlan(ctx), nil
|
|
}
|
|
|
|
func toIPCTickTrace(t loop.TickTrace) ipc.TickTrace {
|
|
rules := make([]ipc.RuleTrace, len(t.RuleTraces))
|
|
for i, r := range t.RuleTraces {
|
|
rules[i] = toIPCRuleTrace(r)
|
|
}
|
|
return ipc.TickTrace{
|
|
Now: t.Now,
|
|
Winner: t.Winner,
|
|
Rules: rules,
|
|
}
|
|
}
|
|
|
|
func toIPCRuleTrace(r loop.RuleTrace) ipc.RuleTrace {
|
|
return ipc.RuleTrace{
|
|
RuleName: r.RuleName,
|
|
Severity: int(r.Severity),
|
|
PredicateResult: r.PredicateResult,
|
|
GateResult: r.GateResult,
|
|
GateBlockedBy: r.GateBlockedBy,
|
|
GateDetail: toIPCGateDetail(r.GateDetail),
|
|
WasSelected: r.WasSelected,
|
|
LostTo: r.LostTo,
|
|
}
|
|
}
|
|
|
|
func toIPCGateDetail(d loop.GateDetail) ipc.GateDetail {
|
|
return ipc.GateDetail{
|
|
SnoozeUntil: d.SnoozeUntil,
|
|
CooldownUntil: d.CooldownUntil,
|
|
QuietHours: d.QuietHours,
|
|
CalendarBusy: d.CalendarBusy,
|
|
Presence: d.Presence,
|
|
InertKeysMissing: d.InertKeysMissing,
|
|
}
|
|
}
|