45b5e16eff
Things arrive at Maven from eight directions — a relayed Android notification on POST /api/ambient, mail candidates from mavmaild, RSS items, changed pages from the crawler, zenmoney and wg reads from mavpoll, CalDAV events, presence probes, meeting transcripts and image descriptions. Each grew its own shape and its own log line, and nothing could answer "what came in today, from where". internal/event is that answer: a flat source-agnostic envelope (Source, Kind, EntityIDs, Title, Body, Priority, OccurredAt, Payload) plus a bounded in-memory journal. Both are pure — Publish and Normalize take `now` as a parameter, so no clock read sits on a path a replay would drive. Adopting it did not touch eight callers, because every intake path already converges on three ipc.CoreAPI methods: WriteFact, WriteNote and CaptureTask. cmd/mavend/intake.go decorates that ONE interface, so mavweb, mavcaldav, mavpoll, mavmaild and the in-core feed/crawl/capture/ vision workers publish envelopes without knowing events exist. The lone exception is cmd/mavend/mail.go, which captures through the store directly and now publishes explicitly. Nothing dispatches on an event. It is a report that something arrived, never an instruction to speak — "a feed item appeared" becoming a notification is the nag this repo refuses. Digestion may read the journal later; it will still go through internal/loop's rules and the severity/presence routing table. Read surface: ipc.MethodRecentEvents (AuthRead, daemon-cached like TickTrace — a bare store cannot serve a ring) and a read-only /events page in mavweb. Production is unchanged when nobody is watching: a nil *event.Bus makes Publish a no-op and newIntakeAPI returns the wrapped API untouched, so config.intake_journal < 0 leaves no decorator on the call path at all. The default is 512 entries; the "off unless configured" rule is for capabilities that reach out, and a bounded in-memory log of writes core already performed reaches nowhere. Verified: make build, make test (go test -race) both clean. New tests cover the envelope and ring (internal/event, 95.7%), the decorator's invariants — a failed write publishes nothing, a deduped capture publishes nothing, OccurredAt is the fact's Ts and not notice time — and the /events page including escaping of feed-supplied titles.
641 lines
22 KiB
Go
641 lines
22 KiB
Go
package ipc
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Client — the module side of the boundary. Wraps a unix-socket connection
|
|
// and satisfies CoreAPI, so a module imports ipc, holds a CoreAPI, and is
|
|
// agnostic to whether it's been wired in-process (tests / daemon-embedded)
|
|
// or over this socket (full topology). The swappability is the seam auth
|
|
// will insert into without touching module code.
|
|
//
|
|
// One Client ⇒ one conn ⇒ one concurrent request at a time. A module that
|
|
// wants parallel requests opens one Client per goroutine; the store is the
|
|
// bottleneck anyway (single writer), so pipelining buys nothing here and a
|
|
// per-Client lock keeps frame interleaving impossible by construction.
|
|
type Client struct {
|
|
conn net.Conn
|
|
path string // kept so a dropped conn can be re-dialed (core restart)
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// errWriteLost marks a conn drop while sending the request frame: the request
|
|
// never reached the server (or the server never saw a complete frame), so
|
|
// retrying is always safe regardless of method — nothing was applied to
|
|
// retry twice.
|
|
var errWriteLost = errors.New("ipc: connection lost before request sent")
|
|
|
|
// errReadLost marks a conn drop while waiting for the reply: the request was
|
|
// sent and may have already been applied server-side before the connection
|
|
// died (core restart mid-request, crash after commit but before reply, etc).
|
|
// Retrying here can double-apply a mutation, which ECOSYSTEM-SPEC.md's
|
|
// "never retry an unknown outcome" invariant forbids. call() only auto-retries
|
|
// this for read-only methods (idempotent by construction); a mutation method
|
|
// returns ErrAmbiguousOutcome instead so the caller can decide — the outcome
|
|
// genuinely is unknown, not safely retryable and not safely reported as failed.
|
|
var errReadLost = errors.New("ipc: connection lost awaiting reply")
|
|
|
|
// ErrAmbiguousOutcome is returned when a mutation's request may or may not
|
|
// have been applied server-side (the connection dropped after the request was
|
|
// sent, before the reply arrived). Callers must not blindly retry — the retry
|
|
// itself could double-apply. Surface this to the user/operator rather than
|
|
// silently treating it as either success or failure.
|
|
var ErrAmbiguousOutcome = errors.New("ipc: mutation outcome unknown (connection lost awaiting reply)")
|
|
|
|
// readOnlyMethods are safe to retry on an ambiguous (post-send) connection
|
|
// loss: replaying a read cannot double-apply anything. Every method not
|
|
// listed here is treated as a mutation for retry purposes — being
|
|
// conservative (refusing to retry) is the safe default for a method added
|
|
// here by omission.
|
|
var readOnlyMethods = map[Method]bool{
|
|
MethodLatestFact: true,
|
|
MethodLatestFactBySource: true,
|
|
MethodSince: true,
|
|
MethodPresence: true,
|
|
MethodListReminders: true,
|
|
MethodRecentOutcomes: true,
|
|
MethodRecentFacts: true,
|
|
MethodCalendarEvents: true,
|
|
MethodRecentNudges: true,
|
|
MethodQueryNotes: true,
|
|
MethodRecentNotes: true,
|
|
MethodLookupTool: true,
|
|
MethodListTools: true,
|
|
MethodListProposedRoutines: true,
|
|
MethodListTasks: true,
|
|
MethodTickTrace: true,
|
|
MethodMorningStatus: true,
|
|
MethodMCPServers: true,
|
|
MethodDayPlan: true,
|
|
MethodRecentEvents: true,
|
|
}
|
|
|
|
// Dial connects to a core socket at path and returns a Client. The module
|
|
// owns its Client lifecycle; Close on shutdown.
|
|
func Dial(path string) (*Client, error) {
|
|
c, err := net.Dial("unix", path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ipc: dial %s: %w", path, err)
|
|
}
|
|
return &Client{conn: c, path: path}, nil
|
|
}
|
|
|
|
func (c *Client) Close() error {
|
|
if c.conn == nil {
|
|
return nil
|
|
}
|
|
return c.conn.Close()
|
|
}
|
|
|
|
// DialWait is Dial with patience: it retries with capped backoff until the
|
|
// socket is reachable or timeout elapses. Core loads models on boot and may
|
|
// come up after its modules (compose depends_on orders container start, not
|
|
// socket readiness), so a module that Dial'd once would crash-loop on a cold
|
|
// start. Every core-dialing module should use this instead of Dial. Mid-life
|
|
// core restarts are handled separately by the Client's own redial-on-drop.
|
|
func DialWait(path string, timeout time.Duration) (*Client, error) {
|
|
deadline := time.Now().Add(timeout)
|
|
delay := 200 * time.Millisecond
|
|
for {
|
|
c, err := Dial(path)
|
|
if err == nil {
|
|
return c, nil
|
|
}
|
|
if time.Now().After(deadline) {
|
|
return nil, err
|
|
}
|
|
time.Sleep(delay)
|
|
if delay < 2*time.Second {
|
|
delay *= 2
|
|
}
|
|
}
|
|
}
|
|
|
|
// call — the single request/response engine. Serialized by c.mu so a frame
|
|
// and its reply always pair up; no interleaving to disambiguate. A wire
|
|
// RpcError is rehydrated into the matching package sentinel (errors.Is works
|
|
// the same as the in-process path — the boundary is transparent to callers).
|
|
func (c *Client) call(ctx context.Context, m Method, params, result any) error {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
// Honor ctx cancellation by closing the conn — a half-sent frame would
|
|
// desync the stream; tearing down is the clean recovery. A fresh Dial
|
|
// is the module's responsibility on the next call (modules are long-lived
|
|
// processes; a dropped conn is recoverable, not fatal).
|
|
select {
|
|
case <-ctx.Done():
|
|
c.drop()
|
|
return ctx.Err()
|
|
default:
|
|
}
|
|
|
|
var raw json.RawMessage
|
|
if params != nil {
|
|
b, err := json.Marshal(params)
|
|
if err != nil {
|
|
return fmt.Errorf("ipc: marshal params: %w", err)
|
|
}
|
|
raw = b
|
|
}
|
|
|
|
var resp Response
|
|
err := c.roundtrip(m, raw, &resp)
|
|
switch {
|
|
case errors.Is(err, errWriteLost):
|
|
// The request never left; a duplicate send can't double-apply.
|
|
// Redial (roundtrip re-dials on a nil conn) and retry exactly once.
|
|
err = c.roundtrip(m, raw, &resp)
|
|
case errors.Is(err, errReadLost):
|
|
if readOnlyMethods[m] {
|
|
// A duplicate read can't double-apply either — safe to replay.
|
|
err = c.roundtrip(m, raw, &resp)
|
|
} else {
|
|
// The mutation may have already committed server-side. Do not
|
|
// retry: report the ambiguity instead of guessing.
|
|
return fmt.Errorf("%w: %v", ErrAmbiguousOutcome, err)
|
|
}
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp.Error != nil {
|
|
return hydrate(resp.Error)
|
|
}
|
|
if result == nil {
|
|
return nil
|
|
}
|
|
// "null" body into a pointer is valid (sets the zero value); marshal a
|
|
// RawMessage directly to avoid extra encode/decode churn.
|
|
return json.Unmarshal(resp.Result, result)
|
|
}
|
|
|
|
// roundtrip sends one request and reads its reply on c.conn, lazily (re)dialing
|
|
// if the conn is nil (fresh Client or a prior drop). A write-phase (or dial)
|
|
// failure is wrapped in errWriteLost (always safe to retry); a read-phase
|
|
// failure is wrapped in errReadLost (ambiguous — call() only retries it for
|
|
// read-only methods). Either way a failed conn is dropped so the next call
|
|
// re-dials clean. Caller holds c.mu.
|
|
func (c *Client) roundtrip(m Method, raw json.RawMessage, resp *Response) error {
|
|
if c.conn == nil {
|
|
conn, err := net.Dial("unix", c.path)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: dial %s: %v", errWriteLost, c.path, err)
|
|
}
|
|
c.conn = conn
|
|
}
|
|
if err := writeFrame(c.conn, Request{Method: m, Params: raw}); err != nil {
|
|
c.drop()
|
|
return fmt.Errorf("%w: %v", errWriteLost, err)
|
|
}
|
|
if err := readFrame(c.conn, resp); err != nil {
|
|
c.drop()
|
|
return fmt.Errorf("%w: %v", errReadLost, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// drop closes and forgets the current conn so the next call re-dials.
|
|
func (c *Client) drop() {
|
|
if c.conn != nil {
|
|
_ = c.conn.Close()
|
|
c.conn = nil
|
|
}
|
|
}
|
|
|
|
// hydrate rehydrates a wire RpcError into the matching package sentinel. The
|
|
// code↔sentinel table is the only place the wire "knows" about errors; keep it
|
|
// in sync with codeOf in wire.go.
|
|
func hydrate(e *RpcError) error {
|
|
switch e.Code {
|
|
case codeNoFact:
|
|
return fmt.Errorf("%w: %s", ErrNoFact, e.Message)
|
|
case codeConfidence:
|
|
return fmt.Errorf("%w: %s", ErrConfidence, e.Message)
|
|
case codeVoidsMissing:
|
|
return fmt.Errorf("%w: %s", ErrVoidsMissing, e.Message)
|
|
case codeNudgeNotFound:
|
|
return fmt.Errorf("%w: %s", ErrNudgeNotFound, e.Message)
|
|
case codeNudgeOutcome:
|
|
return fmt.Errorf("%w: %s", ErrNudgeOutcome, e.Message)
|
|
case codeReminderMissing:
|
|
return fmt.Errorf("%w: %s", ErrReminderNotFound, e.Message)
|
|
case codeReminderState:
|
|
return fmt.Errorf("%w: %s", ErrReminderState, e.Message)
|
|
case codeToolNotFound:
|
|
return fmt.Errorf("%w: %s", ErrToolNotFound, e.Message)
|
|
case codeUnknownMethod:
|
|
return fmt.Errorf("%w: %s", ErrUnknownMethod, e.Message)
|
|
case codeBadParams:
|
|
return fmt.Errorf("%w: %s", ErrBadParams, e.Message)
|
|
case codeForbidden:
|
|
return fmt.Errorf("%w: %s", ErrForbidden, e.Message)
|
|
default:
|
|
return errors.New(e.Error())
|
|
}
|
|
}
|
|
|
|
// CoreAPI implementation on *Client. Each method is a thin call() shim; the
|
|
// shape mirrors the CoreAPI interface 1:1 so the embedded-doc intent (module
|
|
// holds a CoreAPI, transport-agnostic) reads straight off the signatures.
|
|
|
|
func (c *Client) WriteFact(ctx context.Context, req WriteFactReq) (int64, error) {
|
|
var r idResp
|
|
if err := c.call(ctx, MethodWriteFact, req, &r); err != nil {
|
|
return 0, err
|
|
}
|
|
return r.ID, nil
|
|
}
|
|
|
|
func (c *Client) LatestFact(ctx context.Context, key string) (Fact, error) {
|
|
var f Fact
|
|
if err := c.call(ctx, MethodLatestFact, keyReq{Key: key}, &f); err != nil {
|
|
return Fact{}, err
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
func (c *Client) LatestFactBySource(ctx context.Context, key, source string) (Fact, error) {
|
|
var f Fact
|
|
if err := c.call(ctx, MethodLatestFactBySource, keySourceReq{Key: key, Source: source}, &f); err != nil {
|
|
return Fact{}, err
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
func (c *Client) Since(ctx context.Context, key string, now time.Time) (time.Duration, error) {
|
|
var r sinceResp
|
|
if err := c.call(ctx, MethodSince, sinceReq{Key: key, Now: now}, &r); err != nil {
|
|
return 0, err
|
|
}
|
|
return r.Dur, nil
|
|
}
|
|
|
|
func (c *Client) Presence(ctx context.Context) (Presence, error) {
|
|
var p Presence
|
|
if err := c.call(ctx, MethodPresence, nil, &p); err != nil {
|
|
return Presence{}, err
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
func (c *Client) CreateReminder(ctx context.Context, fire time.Time, payload, cron string) (int64, error) {
|
|
var r idResp
|
|
if err := c.call(ctx, MethodCreateReminder, createReminderReq{Fire: fire, Payload: payload, Cron: cron}, &r); err != nil {
|
|
return 0, err
|
|
}
|
|
return r.ID, nil
|
|
}
|
|
|
|
func (c *Client) MarkReminder(ctx context.Context, id int64, status string) error {
|
|
return c.call(ctx, MethodMarkReminder, markReminderReq{ID: id, Status: status}, nil)
|
|
}
|
|
|
|
func (c *Client) ListReminders(ctx context.Context, n int) ([]Reminder, error) {
|
|
var out []Reminder
|
|
if err := c.call(ctx, MethodListReminders, nReq{N: n}, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (c *Client) RecordNudge(ctx context.Context, rule, channel, message string, ts time.Time) (int64, error) {
|
|
var r idResp
|
|
if err := c.call(ctx, MethodRecordNudge, recordNudgeReq{Rule: rule, Channel: channel, Message: message, Ts: ts}, &r); err != nil {
|
|
return 0, err
|
|
}
|
|
return r.ID, nil
|
|
}
|
|
|
|
func (c *Client) ResolveNudge(ctx context.Context, id int64, outcome string, ts time.Time) error {
|
|
return c.call(ctx, MethodResolveNudge, resolveNudgeReq{ID: id, Outcome: outcome, Ts: ts}, nil)
|
|
}
|
|
|
|
func (c *Client) RecentOutcomes(ctx context.Context, rule string, n int) ([]string, error) {
|
|
var out []string
|
|
if err := c.call(ctx, MethodRecentOutcomes, outcomesReq{Rule: rule, N: n}, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (c *Client) RecentFacts(ctx context.Context, n int) ([]Fact, error) {
|
|
var out []Fact
|
|
if err := c.call(ctx, MethodRecentFacts, nReq{N: n}, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (c *Client) CalendarEvents(ctx context.Context, from, to time.Time) ([]Fact, error) {
|
|
var out []Fact
|
|
if err := c.call(ctx, MethodCalendarEvents, calendarEventsReq{From: from, To: to}, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (c *Client) RecentNudges(ctx context.Context, n int) ([]Nudge, error) {
|
|
var out []Nudge
|
|
if err := c.call(ctx, MethodRecentNudges, nReq{N: n}, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (c *Client) WriteNote(ctx context.Context, ts time.Time, text string, embedding []float32, source string) (int64, error) {
|
|
var r idResp
|
|
if err := c.call(ctx, MethodWriteNote, writeNoteReq{Ts: ts, Text: text, Embedding: embedding, Source: source}, &r); err != nil {
|
|
return 0, err
|
|
}
|
|
return r.ID, nil
|
|
}
|
|
|
|
func (c *Client) QueryNotes(ctx context.Context, embedding []float32, k int) ([]Note, error) {
|
|
var out []Note
|
|
if err := c.call(ctx, MethodQueryNotes, queryNotesReq{Embedding: embedding, K: k}, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (c *Client) RecentNotes(ctx context.Context, n int) ([]Note, error) {
|
|
var out []Note
|
|
if err := c.call(ctx, MethodRecentNotes, nReq{N: n}, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (c *Client) ProposeTool(ctx context.Context, name, utterance, scope string, ts time.Time) (bool, error) {
|
|
var r proposeToolResp
|
|
if err := c.call(ctx, MethodProposeTool, proposeToolReq{Name: name, Scope: scope, Utterance: utterance, Ts: ts}, &r); err != nil {
|
|
return false, err
|
|
}
|
|
return r.Proposed, nil
|
|
}
|
|
|
|
func (c *Client) EnableTool(ctx context.Context, name string, cmd []string, destructive bool, scope string, ts time.Time) error {
|
|
return c.call(ctx, MethodEnableTool, enableToolReq{Name: name, Scope: scope, Cmd: cmd, Destructive: destructive, Ts: ts}, nil)
|
|
}
|
|
|
|
func (c *Client) DisableTool(ctx context.Context, name string) error {
|
|
return c.call(ctx, MethodDisableTool, disableToolReq{Name: name}, nil)
|
|
}
|
|
|
|
func (c *Client) AssertStepUp(ctx context.Context) error {
|
|
return c.call(ctx, MethodAssertStepUp, nil, nil)
|
|
}
|
|
|
|
// StoreEncryptionKey wraps the daemon's at-rest key under secret, the 32-byte
|
|
// WebAuthn PRF output for the freshly enrolled credential.
|
|
func (c *Client) StoreEncryptionKey(ctx context.Context, secret []byte) error {
|
|
return c.call(ctx, MethodStoreEncryptionKey, storeEncryptionKeyReq{Secret: secret}, nil)
|
|
}
|
|
|
|
// Unlock hands the daemon the PRF secret so it can unwrap its at-rest key and
|
|
// open the store. Refused unless a passkey assertion was verified first.
|
|
func (c *Client) Unlock(ctx context.Context, secret []byte) error {
|
|
return c.call(ctx, MethodUnlock, unlockReq{Secret: secret}, nil)
|
|
}
|
|
|
|
func (c *Client) LookupTool(ctx context.Context, name string) (Tool, error) {
|
|
var t Tool
|
|
if err := c.call(ctx, MethodLookupTool, lookupToolReq{Name: name}, &t); err != nil {
|
|
return Tool{}, err
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
func (c *Client) ListTools(ctx context.Context, status string) ([]Tool, error) {
|
|
var r listToolsResp
|
|
if err := c.call(ctx, MethodListTools, listToolsReq{Status: status}, &r); err != nil {
|
|
return nil, err
|
|
}
|
|
return r.Tools, nil
|
|
}
|
|
|
|
func (c *Client) DeleteTool(ctx context.Context, name string) error {
|
|
return c.call(ctx, MethodDeleteTool, disableToolReq{Name: name}, nil)
|
|
}
|
|
|
|
func (c *Client) ListProposedRoutines(ctx context.Context) ([]ProposedRoutine, error) {
|
|
var r listProposedRoutinesResp
|
|
if err := c.call(ctx, MethodListProposedRoutines, nil, &r); err != nil {
|
|
return nil, err
|
|
}
|
|
return r.Routines, nil
|
|
}
|
|
|
|
func (c *Client) CaptureTask(ctx context.Context, req CaptureTaskReq) (CaptureTaskResp, error) {
|
|
var r CaptureTaskResp
|
|
if err := c.call(ctx, MethodCaptureTask, req, &r); err != nil {
|
|
return CaptureTaskResp{}, err
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
func (c *Client) ListTasks(ctx context.Context, status string) ([]Task, error) {
|
|
var r listTasksResp
|
|
if err := c.call(ctx, MethodListTasks, listTasksReq{Status: status}, &r); err != nil {
|
|
return nil, err
|
|
}
|
|
return r.Tasks, nil
|
|
}
|
|
|
|
func (c *Client) SetTaskStatus(ctx context.Context, id int64, status string, ts time.Time) error {
|
|
return c.call(ctx, MethodSetTaskStatus, setTaskStatusReq{ID: id, Status: status, Ts: ts}, nil)
|
|
}
|
|
|
|
// IngestMail hands one fetched message to core for extraction. ErrUnknownMethod
|
|
// means core has no email block configured — the caller should stop asking, not
|
|
// retry.
|
|
func (c *Client) IngestMail(ctx context.Context, req IngestMailReq) (IngestMailResp, error) {
|
|
var r IngestMailResp
|
|
if err := c.call(ctx, MethodIngestMail, req, &r); err != nil {
|
|
return IngestMailResp{}, err
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
// DescribeImage hands one image to core to look at (Vikunja #252).
|
|
// ErrUnknownMethod means core has no media store or vision is off — the caller
|
|
// should stop asking, not retry. A response with an ID and an empty Description
|
|
// means the bytes were stored but nothing could describe them yet, which is the
|
|
// expected state on a box with no vision model on disk.
|
|
func (c *Client) DescribeImage(ctx context.Context, req DescribeImageReq) (DescribeImageResp, error) {
|
|
var r DescribeImageResp
|
|
if err := c.call(ctx, MethodDescribeImage, req, &r); err != nil {
|
|
return DescribeImageResp{}, err
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
// CaptureStart begins recording a meeting (Vikunja #253). ErrUnknownMethod
|
|
// means the operator has not enabled capture — the caller should say so and stop
|
|
// asking, not retry.
|
|
func (c *Client) CaptureStart(ctx context.Context, req CaptureStartReq) (CaptureStartResp, error) {
|
|
var r CaptureStartResp
|
|
if err := c.call(ctx, MethodCaptureStart, req, &r); err != nil {
|
|
return CaptureStartResp{}, err
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
// CaptureAppend hands one chunk of audio to the running session. An error means
|
|
// the frame was not kept: either nothing is being recorded, or the session hit
|
|
// its time limit. Either way the client stops sending.
|
|
func (c *Client) CaptureAppend(ctx context.Context, req CaptureAppendReq) (CaptureAppendResp, error) {
|
|
var r CaptureAppendResp
|
|
if err := c.call(ctx, MethodCaptureAppend, req, &r); err != nil {
|
|
return CaptureAppendResp{}, err
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
// CaptureStop ends the session. Slow — it transcribes and summarises the whole
|
|
// recording — so pass a context with room. Set Discard to throw the recording
|
|
// away instead.
|
|
func (c *Client) CaptureStop(ctx context.Context, req CaptureStopReq) (CaptureStopResp, error) {
|
|
var r CaptureStopResp
|
|
if err := c.call(ctx, MethodCaptureStop, req, &r); err != nil {
|
|
return CaptureStopResp{}, err
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
// CaptureStatus reports the running session, if any.
|
|
func (c *Client) CaptureStatus(ctx context.Context) (CaptureStatusResp, error) {
|
|
var r CaptureStatusResp
|
|
if err := c.call(ctx, MethodCaptureStatus, nil, &r); err != nil {
|
|
return CaptureStatusResp{}, err
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
// EnrollSpeaker registers a voice from several deliberately recorded samples
|
|
// (Vikunja #255). ErrUnknownMethod means no speaker block is configured, which
|
|
// is the default: on an unconfigured box there is no way to take a voiceprint.
|
|
func (c *Client) EnrollSpeaker(ctx context.Context, req EnrollSpeakerReq) (EnrollSpeakerResp, error) {
|
|
var r EnrollSpeakerResp
|
|
if err := c.call(ctx, MethodEnrollSpeaker, req, &r); err != nil {
|
|
return EnrollSpeakerResp{}, err
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
// ListSpeakers reports who is enrolled. The voiceprints themselves stay in
|
|
// core. Enabled is false when profiles exist but no embedding model is wired,
|
|
// so a surface can say "enrolled, not recognising" rather than implying Maven
|
|
// knows who is talking.
|
|
func (c *Client) ListSpeakers(ctx context.Context) (ListSpeakersResp, error) {
|
|
var r ListSpeakersResp
|
|
if err := c.call(ctx, MethodListSpeakers, nil, &r); err != nil {
|
|
return ListSpeakersResp{}, err
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
// ForgetSpeaker deletes one voiceprint.
|
|
func (c *Client) ForgetSpeaker(ctx context.Context, id string) error {
|
|
return c.call(ctx, MethodForgetSpeaker, ForgetSpeakerReq{ID: id}, nil)
|
|
}
|
|
|
|
// SwapModel asks core to load another resident model (Vikunja #250).
|
|
// ErrUnknownMethod means core has no phraser.swap_models allowlist configured;
|
|
// ErrForbidden means the path is not on it, or step-up was not asserted. A
|
|
// non-nil error with RolledBack set means nothing changed — the old model is
|
|
// still serving.
|
|
func (c *Client) SwapModel(ctx context.Context, req SwapModelReq) (SwapModelResp, error) {
|
|
var r SwapModelResp
|
|
if err := c.call(ctx, MethodSwapModel, req, &r); err != nil {
|
|
return SwapModelResp{}, err
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
// ModelStatus reports the resident model and the swap allowlist. Read-only.
|
|
func (c *Client) ModelStatus(ctx context.Context) (ModelStatusResp, error) {
|
|
var r ModelStatusResp
|
|
if err := c.call(ctx, MethodModelStatus, nil, &r); err != nil {
|
|
return ModelStatusResp{}, err
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
func (c *Client) DismissProposedRoutine(ctx context.Context, id int64) error {
|
|
return c.call(ctx, MethodDismissProposedRoutine, dismissProposedRoutineReq{ID: id}, nil)
|
|
}
|
|
|
|
func (c *Client) AcceptProposedRoutine(ctx context.Context, id int64) error {
|
|
return c.call(ctx, MethodAcceptProposedRoutine, acceptProposedRoutineReq{ID: id}, nil)
|
|
}
|
|
|
|
func (c *Client) Chat(ctx context.Context, text string) (string, error) {
|
|
var r chatResp
|
|
if err := c.call(ctx, MethodChat, chatReq{Text: text}, &r); err != nil {
|
|
return "", err
|
|
}
|
|
return r.Reply, nil
|
|
}
|
|
|
|
func (c *Client) TickTrace(ctx context.Context) (TickTrace, error) {
|
|
var t TickTrace
|
|
if err := c.call(ctx, MethodTickTrace, nil, &t); err != nil {
|
|
return TickTrace{}, err
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
func (c *Client) RecentEvents(ctx context.Context, n int) ([]IntakeEvent, error) {
|
|
var e []IntakeEvent
|
|
if err := c.call(ctx, MethodRecentEvents, nReq{N: n}, &e); err != nil {
|
|
return nil, err
|
|
}
|
|
return e, nil
|
|
}
|
|
|
|
func (c *Client) MCPServers(ctx context.Context) ([]MCPServerStatus, error) {
|
|
var s []MCPServerStatus
|
|
if err := c.call(ctx, MethodMCPServers, nil, &s); err != nil {
|
|
return nil, err
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
func (c *Client) MorningStatus(ctx context.Context) ([]MorningRoutineStatus, error) {
|
|
var s []MorningRoutineStatus
|
|
if err := c.call(ctx, MethodMorningStatus, nil, &s); err != nil {
|
|
return nil, err
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
func (c *Client) DayPlan(ctx context.Context) (DayPlan, error) {
|
|
var p DayPlan
|
|
if err := c.call(ctx, MethodDayPlan, nil, &p); err != nil {
|
|
return DayPlan{}, err
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
func (c *Client) RevertFact(ctx context.Context, key string) (int64, error) {
|
|
var result struct {
|
|
NewID int64 `json:"new_id"`
|
|
}
|
|
if err := c.call(ctx, MethodRevertFact, map[string]string{"key": key}, &result); err != nil {
|
|
return 0, err
|
|
}
|
|
return result.NewID, nil
|
|
}
|
|
|
|
// Compile-time check: *Client satisfies CoreAPI.
|
|
var _ CoreAPI = (*Client)(nil)
|