57161fb762
Nothing at read time told a feed item or a crawled page apart from his own notes. QueryNotes ranked every note by cosine and the notes answer handed the nearest five to the phraser, so "что я говорил про переезд" could be answered out of a stranger's web page, prefixed with "вот что я нашла: ". Recall now excludes the read sources, rss: and crawl:, and the list is one place. The feed answer needed a different read as a result, and it needed one anyway: it scanned the last 200 notes of any source, so a busy day of voice notes pushed the newest headline out of the window and she said "в лентах пока ничего нового" while the poller was working fine. RecentNotesFromSource asks for feed notes by source, so the window holds 200 of them. Found in review of #66 and #67.
662 lines
23 KiB
Go
662 lines
23 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,
|
|
MethodRecentNotesFromSource: true,
|
|
MethodLookupTool: true,
|
|
MethodListTools: true,
|
|
MethodListProposedRoutines: true,
|
|
MethodListTasks: true,
|
|
MethodTickTrace: true,
|
|
MethodMorningStatus: true,
|
|
MethodMCPServers: true,
|
|
MethodDayPlan: true,
|
|
MethodRecentEvents: true,
|
|
MethodPing: 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) RecentNotesFromSource(ctx context.Context, prefix string, n int) ([]Note, error) {
|
|
var out []Note
|
|
if err := c.call(ctx, MethodRecentNotesFromSource, sourceNReq{Prefix: prefix, 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
|
|
}
|
|
|
|
// Ping asks whether the daemon is there, and whether it is locked. It is not a
|
|
// CoreAPI method: it touches no store, so it answers before the passkey
|
|
// assertion that every other read waits for.
|
|
func (c *Client) Ping(ctx context.Context) (PingResp, error) {
|
|
var r PingResp
|
|
if err := c.call(ctx, MethodPing, nil, &r); err != nil {
|
|
return PingResp{}, err
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
// Compile-time check: *Client satisfies CoreAPI.
|
|
var _ CoreAPI = (*Client)(nil)
|