d3c63e6493
The pattern detector needs four events for one action+object spread by at least two hours before it proposes a routine. The only writer in the tree is a fact write at time.Now(), so V-43, V-46, V-247 and V-254 all stopped at the same missing step. This is the wire half of the seam that unblocks them. The request takes a fact — key, value, timestamp — not an event, so pattern.Extract runs for real on the daemon side and a key the extractor ignores seeds nothing. The response says which of those happened, because a caller that assumed a seed always yields an event would read four silent successes as a broken detector. AuthStepUp, the same rung as mutating the tool allowlist, and not because backdating is privileged in the usual sense: every other write records when something happened and this one asserts it. StoreAPI refuses outright — the method needs the daemon's detect-and-propose step, and a direct store caller would write a fact and quietly skip it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011x5DgnExQ5XZy8TZPs5bot
202 lines
10 KiB
Go
202 lines
10 KiB
Go
package ipc
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
)
|
|
|
|
// CoreAPI and the eight domain interfaces it composes (Vikunja #408).
|
|
//
|
|
// It used to be one flat block of forty methods, and the cost of that shape
|
|
// was paid three times over: every method added an arm to the dispatcher, a
|
|
// stub to the daemon's locked API, and a stub to every test double. Two of
|
|
// those three are already gone — the dispatcher is a table (methodTable in
|
|
// server.go), and UnimplementedCoreAPI took the padding out of the doubles and
|
|
// out of lockedAPI, which no longer exists.
|
|
//
|
|
// What was left is the interface itself, and this file is that half. CoreAPI
|
|
// is unchanged as a type: the same forty methods, in the same order, so the
|
|
// wire contract, the client proxy and the store adapter are all untouched.
|
|
// What it gains is a named seam per domain, so a caller that only reads facts
|
|
// can say FactAPI and a reader can see which cluster a method belongs to
|
|
// without counting lines.
|
|
//
|
|
// Add a method to the domain it belongs to, not to CoreAPI.
|
|
|
|
// FactAPI — the fact store: write, read the current value, read history, and
|
|
// undo. Presence sits here because it is a hysteresis view over presence
|
|
// facts, not a store of its own.
|
|
type FactAPI interface {
|
|
WriteFact(ctx context.Context, req WriteFactReq) (int64, error)
|
|
LatestFact(ctx context.Context, key string) (Fact, error)
|
|
LatestFactBySource(ctx context.Context, key, source string) (Fact, error)
|
|
Since(ctx context.Context, key string, now time.Time) (time.Duration, error)
|
|
Presence(ctx context.Context) (Presence, error)
|
|
RecentFacts(ctx context.Context, n int) ([]Fact, error)
|
|
RecentActiveFactsByKind(ctx context.Context, kind string, n int) ([]Fact, error)
|
|
CalendarEvents(ctx context.Context, from, to time.Time) ([]Fact, error)
|
|
RevertFact(ctx context.Context, key string) (int64, error)
|
|
}
|
|
|
|
// ReminderAPI — scheduled sends the owner asked for.
|
|
type ReminderAPI interface {
|
|
CreateReminder(ctx context.Context, fire time.Time, payload, cron string) (int64, error)
|
|
MarkReminder(ctx context.Context, id int64, status string) error
|
|
ListReminders(ctx context.Context, n int) ([]Reminder, error)
|
|
}
|
|
|
|
// NudgeAPI — proactive sends Maven proposed, their outcomes, and the outbox
|
|
// they went out through.
|
|
type NudgeAPI interface {
|
|
RecordNudge(ctx context.Context, rule, channel, message string, ts time.Time) (int64, error)
|
|
ResolveNudge(ctx context.Context, id int64, outcome string, ts time.Time) error
|
|
RecentOutcomes(ctx context.Context, rule string, n int) ([]string, error)
|
|
RecentNudges(ctx context.Context, n int) ([]Nudge, error)
|
|
// DeliveryAttempts reads the outbox, newest first. An empty status means
|
|
// every status (Vikunja #390).
|
|
DeliveryAttempts(ctx context.Context, status string, n int) ([]DeliveryAttempt, error)
|
|
}
|
|
|
|
// NoteAPI — free text he captured, plus the embedded recall over it.
|
|
type NoteAPI interface {
|
|
WriteNote(ctx context.Context, ts time.Time, text string, embedding []float32, source string) (int64, error)
|
|
QueryNotes(ctx context.Context, embedding []float32, k int) ([]Note, error)
|
|
RecentNotes(ctx context.Context, n int) ([]Note, error)
|
|
// RecentNotesFromSource — the newest n notes whose source starts with
|
|
// prefix. Notes Maven read rather than heard (rss:, crawl:) are excluded
|
|
// from recall, so this is the only way to reach them, and it keeps the feed
|
|
// answer from being crowded out of a fixed window by his own notes.
|
|
RecentNotesFromSource(ctx context.Context, prefix string, n int) ([]Note, error)
|
|
}
|
|
|
|
// ToolAPI — the capability allowlist and its read side.
|
|
//
|
|
// ProposeTool drafts an inert 'proposed' tool scaffold (maven-callable);
|
|
// returns whether a new proposal was written. EnableTool fills cmd +
|
|
// destructive and flips status to 'enabled'. DisableTool reverts an
|
|
// enabled tool back to proposed (it stays in the store, won't run).
|
|
// Enable/DisableTool gate at AuthStepUp (allowlist mutation, human-only);
|
|
// ProposeTool is maven-callable (no step-up — she has no passkey).
|
|
// LookupTool/ListTools read them.
|
|
// scope defaults to "homelab" when empty.
|
|
type ToolAPI interface {
|
|
ProposeTool(ctx context.Context, name, utterance, scope string, ts time.Time) (bool, error)
|
|
EnableTool(ctx context.Context, name string, cmd []string, destructive bool, scope string, ts time.Time) error
|
|
DisableTool(ctx context.Context, name string) error
|
|
DeleteTool(ctx context.Context, name string) error
|
|
LookupTool(ctx context.Context, name string) (Tool, error)
|
|
ListTools(ctx context.Context, status string) ([]Tool, error)
|
|
|
|
// MCPServers reports the configured MCP servers and their health
|
|
// (Vikunja #251). Read-only introspection for /tools — there is no
|
|
// "call this tool" method on purpose: an MCP tool runs through the same
|
|
// allowlist, confirm turn and act path as any other tool, and a second
|
|
// mutation path would be a second thing to get wrong. Empty when the
|
|
// mcp config block is absent, which is the default.
|
|
MCPServers(ctx context.Context) ([]MCPServerStatus, error)
|
|
}
|
|
|
|
// RoutineAPI — the shapes of his day: routines Maven noticed and proposed, the
|
|
// morning checklist, and today's plan.
|
|
//
|
|
// MorningStatus and DayPlan are daemon-computed rather than stored, so the
|
|
// store adapter returns an error for both — the same shape as TickTrace.
|
|
type RoutineAPI interface {
|
|
// ListProposedRoutines returns proposed routines, newest first.
|
|
ListProposedRoutines(ctx context.Context) ([]ProposedRoutine, error)
|
|
// DismissProposedRoutine flips a proposed routine to 'dismissed'.
|
|
DismissProposedRoutine(ctx context.Context, id int64) error
|
|
// AcceptProposedRoutine flips a proposed routine to 'accepted'. The tick
|
|
// loop takes the schedule from there — no reminder is created (Vikunja #366).
|
|
AcceptProposedRoutine(ctx context.Context, id int64) error
|
|
|
|
// SeedEvent writes a backdated fact and runs extraction and detection over
|
|
// it, so a proposal can be produced in one sitting rather than over real
|
|
// days (Vikunja #518). See SeedEventReq for why this exists and what keeps
|
|
// it shut. Daemon-computed, like MorningStatus — the store adapter refuses
|
|
// it, because the detect-and-propose step lives in mavend.
|
|
SeedEvent(ctx context.Context, req SeedEventReq) (SeedEventResp, error)
|
|
|
|
// MorningStatus returns each configured morning routine's current
|
|
// checklist state (see internal/morning): active today/now, which items
|
|
// are done, which are still missing.
|
|
MorningStatus(ctx context.Context) ([]MorningRoutineStatus, error)
|
|
|
|
// DayPlan returns today's ordered plan — calendar events, pending
|
|
// reminders and any morning checklist still outstanding (see
|
|
// internal/morning.BuildPlan) — plus the spoken RU rendering of it.
|
|
// Read-only: asking for the plan never dispatches or schedules anything.
|
|
DayPlan(ctx context.Context) (DayPlan, error)
|
|
}
|
|
|
|
// TaskAPI — outstanding work, whatever surface it arrived from.
|
|
type TaskAPI interface {
|
|
// CaptureTask records a task. See CaptureTaskReq — this is the single
|
|
// intake seam for the voice path, the web form and the future email
|
|
// extractor. Idempotent per live normalised text; the response says
|
|
// whether a row was actually created.
|
|
CaptureTask(ctx context.Context, req CaptureTaskReq) (CaptureTaskResp, error)
|
|
// ListTasks returns tasks in one status, newest first. "" is every row,
|
|
// "live" is candidate + open (outstanding work).
|
|
ListTasks(ctx context.Context, status string) ([]Task, error)
|
|
// SetTaskStatus moves a task forward once: candidate→open|dropped,
|
|
// open→done|dropped. Any other move is refused.
|
|
SetTaskStatus(ctx context.Context, id int64, status string, ts time.Time, by string) error
|
|
}
|
|
|
|
// SystemAPI — what the daemon knows about itself, plus the one method that
|
|
// runs a whole turn.
|
|
type SystemAPI interface {
|
|
// TickTrace returns the most recent tick's rule trace. The daemon caches
|
|
// this after every tick; the store adapter returns an error (trace is not
|
|
// persisted — it's a daemon-level cache).
|
|
TickTrace(ctx context.Context) (TickTrace, error)
|
|
|
|
// RecentEcosystemTraces reads the ecosystem call log, which lives in its
|
|
// own table so machine-rate traces never crowd out human-rate facts.
|
|
RecentEcosystemTraces(ctx context.Context, n int) ([]EcosystemTrace, error)
|
|
|
|
// RecentEvents returns the daemon's unified intake journal, newest first
|
|
// (Vikunja #283) — one envelope per thing that arrived, whatever direction
|
|
// it came from: a relayed notification, a mail candidate, a feed item, a
|
|
// changed page, a spend, a presence probe.
|
|
//
|
|
// Read-only and daemon-cached, the same shape as TickTrace and DayPlan:
|
|
// the store adapter returns an error, because the journal is a bounded
|
|
// in-memory ring and not a table. Its contents are a window over intake,
|
|
// never the durable record — that is still the fact, note or task the
|
|
// intake path wrote.
|
|
RecentEvents(ctx context.Context, n int) ([]IntakeEvent, error)
|
|
|
|
// Chat routes a text utterance through the reactive handler's core path
|
|
// (router → dialogue → action → replier) and returns the reply text.
|
|
// No audio or stt/tts — for text channels (mavweb, telegram).
|
|
//
|
|
// conversation names the thread. A parked clarifying question is held per
|
|
// conversation, so an unanswered question on one reach cannot eat the next
|
|
// utterance from another (Vikunja #466). Empty means the unattributed text
|
|
// tap and is still one conversation of its own, separate from the mic.
|
|
Chat(ctx context.Context, conversation, text string) (string, error)
|
|
}
|
|
|
|
// CoreAPI — what core exposes to modules. One Go interface, satisfied by:
|
|
// - the in-process store adapter (storeapi.go) — used by the daemon
|
|
// for modules that live in-process for now (router, delivery) and by tests,
|
|
// - the socket-backed server's dispatcher (which delegates to a CoreAPI),
|
|
// - the client proxy (client.go) — same interface, over the wire.
|
|
//
|
|
// So a module imports ipc, holds a CoreAPI, and is agnostic to whether it's
|
|
// been wired in-process (tests / daemon-embedded) or socketed (full topology).
|
|
// That swappability is the seam the auth layer inserts into without touching
|
|
// the module code.
|
|
type CoreAPI interface {
|
|
FactAPI
|
|
ReminderAPI
|
|
NudgeAPI
|
|
NoteAPI
|
|
ToolAPI
|
|
RoutineAPI
|
|
TaskAPI
|
|
SystemAPI
|
|
}
|