888c1c6768
V-539 said SearXNG claims every world question, including invented terms, so Kiwix is never reached. Measured today against the configured instance: seven of eight invented Russian questions now return zero results, and Response.Empty() already passes those to the ZIM. The premise moved with the upstream engine set in three days. The three quality signals the task named were recorded per query and none separate the sets. Token overlap is zero for the one bad claim and also zero for "столица Франции", whose answer is Париж. Empty snippets never fire, because ParseResponse already drops a hit with no text. SearXNG returned no corrections or suggestions even for the query it silently respelled. So no threshold is built: it would cost a real answer to save one invented word. What ships is the second half. The claiming query source crosses the IPC seam on ipc.ChatReply.Source and renders as a badge beside the reply on /chat. It rides the context rather than a return value, because handleText answers every reach through one string and the mic, telegram and the web all share it. Chat now returns ChatReply instead of a bare string. Full -race suite green.
123 lines
4.0 KiB
Go
123 lines
4.0 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
|
|
}
|
|
|
|
// Chat runs one text turn and reports which query source claimed it. The sink
|
|
// rides the context so handleText keeps the one string signature the mic,
|
|
// telegram and the web all call it through (V-539).
|
|
func (d *daemonAPI) Chat(ctx context.Context, conversation, text string) (ipc.ChatReply, error) {
|
|
if d.chatFn == nil {
|
|
return ipc.ChatReply{}, errors.New("mavend: chat not available")
|
|
}
|
|
ctx, sink := withQuerySourceSink(ctx)
|
|
reply := d.chatFn(ctx, conversation, text)
|
|
return ipc.ChatReply{Reply: reply, Source: sink.Name()}, 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,
|
|
}
|
|
}
|