Files
Maven/internal/ipc/client.go
T
2026-07-03 00:32:48 +02:00

262 lines
8.3 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
mu sync.Mutex
}
// 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}, nil
}
func (c *Client) Close() error { return c.conn.Close() }
// 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.conn.Close()
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
}
if err := writeFrame(c.conn, Request{Method: m, Params: raw}); err != nil {
return err
}
var resp Response
if err := readFrame(c.conn, &resp); 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)
}
// 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 string) (int64, error) {
var r idResp
if err := c.call(ctx, MethodCreateReminder, createReminderReq{Fire: fire, Payload: payload}, &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) 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) 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 string, ts time.Time) (bool, error) {
var r proposeToolResp
if err := c.call(ctx, MethodProposeTool, proposeToolReq{Name: name, 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, ts time.Time) error {
return c.call(ctx, MethodEnableTool, enableToolReq{Name: name, Cmd: cmd, Destructive: destructive, Ts: ts}, 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
}
// Compile-time check: *Client satisfies CoreAPI.
var _ CoreAPI = (*Client)(nil)