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.
162 lines
6.3 KiB
Go
162 lines
6.3 KiB
Go
package ipc
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
// Method — one RPC verb. The set is intentionally small: it mirrors exactly
|
|
// what a module legitimately needs from core state, and nothing more. Adding
|
|
// a method is a core-authority change (every method is a new thing a module
|
|
// can ask for); do it deliberately.
|
|
type Method string
|
|
|
|
const (
|
|
MethodWriteFact Method = "write_fact"
|
|
MethodLatestFact Method = "latest_fact"
|
|
MethodLatestFactBySource Method = "latest_fact_by_source"
|
|
MethodSince Method = "since"
|
|
MethodPresence Method = "presence"
|
|
MethodCreateReminder Method = "create_reminder"
|
|
MethodMarkReminder Method = "mark_reminder"
|
|
MethodListReminders Method = "list_reminders"
|
|
MethodRecordNudge Method = "record_nudge"
|
|
MethodResolveNudge Method = "resolve_nudge"
|
|
MethodRecentOutcomes Method = "recent_outcomes"
|
|
MethodRecentFacts Method = "recent_facts"
|
|
MethodCalendarEvents Method = "calendar_events"
|
|
MethodRecentNudges Method = "recent_nudges"
|
|
MethodWriteNote Method = "write_note"
|
|
MethodQueryNotes Method = "query_notes"
|
|
MethodRecentNotes Method = "recent_notes"
|
|
MethodProposeTool Method = "propose_tool"
|
|
MethodEnableTool Method = "enable_tool"
|
|
MethodDisableTool Method = "disable_tool"
|
|
MethodAssertStepUp Method = "assert_stepup"
|
|
MethodStoreEncryptionKey Method = "store_encryption_key"
|
|
MethodUnlock Method = "unlock"
|
|
MethodLookupTool Method = "lookup_tool"
|
|
MethodListTools Method = "list_tools"
|
|
MethodDeleteTool Method = "delete_tool"
|
|
MethodListProposedRoutines Method = "list_proposed_routines"
|
|
MethodDismissProposedRoutine Method = "dismiss_proposed_routine"
|
|
MethodAcceptProposedRoutine Method = "accept_proposed_routine"
|
|
MethodRevertFact Method = "revert_fact"
|
|
MethodTickTrace Method = "tick_trace"
|
|
MethodMorningStatus Method = "morning_status"
|
|
MethodMCPServers Method = "mcp_servers"
|
|
MethodDayPlan Method = "day_plan"
|
|
MethodChat Method = "chat"
|
|
MethodCaptureTask Method = "capture_task"
|
|
MethodListTasks Method = "list_tasks"
|
|
MethodSetTaskStatus Method = "set_task_status"
|
|
MethodIngestMail Method = "ingest_mail"
|
|
MethodSwapModel Method = "swap_model"
|
|
MethodModelStatus Method = "model_status"
|
|
MethodDescribeImage Method = "describe_image"
|
|
MethodCaptureStart Method = "capture_start"
|
|
MethodCaptureAppend Method = "capture_append"
|
|
MethodCaptureStop Method = "capture_stop"
|
|
MethodCaptureStatus Method = "capture_status"
|
|
MethodEnrollSpeaker Method = "enroll_speaker"
|
|
MethodListSpeakers Method = "list_speakers"
|
|
MethodForgetSpeaker Method = "forget_speaker"
|
|
MethodRecentEvents Method = "recent_events"
|
|
)
|
|
|
|
// Request — one frame from module to core. Params is the JSON-encoded argument
|
|
// struct for Method (see api.go for the per-method shapes). The server
|
|
// unmarshals Params based on Method; an unknown Method ⇒ ErrUnknownMethod.
|
|
type Request struct {
|
|
Method Method `json:"m"`
|
|
Params json.RawMessage `json:"p,omitempty"`
|
|
}
|
|
|
|
// Response — one frame from core back to module. Exactly one of Result/Error
|
|
// is set. Result is the JSON-encoded return value of the method (might be a
|
|
// scalar, a struct, or null for void methods).
|
|
type Response struct {
|
|
Result json.RawMessage `json:"r,omitempty"`
|
|
Error *RpcError `json:"e,omitempty"`
|
|
}
|
|
|
|
// RpcError — a typed wire error. Code is one of the sentinel codes below;
|
|
// the client rehydrates it into the matching package sentinel so callers can
|
|
// use errors.Is like they would in-process (core's contract is the same on
|
|
// both sides of the wire — the boundary shouldn't change error semantics).
|
|
type RpcError struct {
|
|
Code string `json:"c"`
|
|
Message string `json:"m,omitempty"`
|
|
}
|
|
|
|
func (e *RpcError) Error() string {
|
|
if e.Message != "" {
|
|
return fmt.Sprintf("ipc: %s: %s", e.Code, e.Message)
|
|
}
|
|
return fmt.Sprintf("ipc: %s", e.Code)
|
|
}
|
|
|
|
// Sentinel codes. Stable over the wire — do not rename. Mirror the package
|
|
// sentinels in api.go 1:1. The string is the contract.
|
|
const (
|
|
codeNoFact = "no_fact"
|
|
codeConfidence = "confidence"
|
|
codeVoidsMissing = "voids_missing"
|
|
codeNudgeNotFound = "nudge_not_found"
|
|
codeNudgeOutcome = "nudge_outcome"
|
|
codeReminderMissing = "reminder_not_found"
|
|
codeReminderState = "reminder_state"
|
|
codeToolNotFound = "tool_not_found"
|
|
codeUnknownMethod = "unknown_method"
|
|
codeBadParams = "bad_params"
|
|
codeForbidden = "forbidden"
|
|
codeInternal = "internal"
|
|
)
|
|
|
|
// codeOf maps a server-side sentinel to its wire code. Anything not matched
|
|
// is codeInternal — we never leak internal Go error text to a module; it
|
|
// gets a generic "internal" and the daemon logs the real error server-side.
|
|
func codeOf(err error) string {
|
|
switch {
|
|
case err == nil:
|
|
return ""
|
|
case errors.Is(err, ErrNoFact):
|
|
return codeNoFact
|
|
case errors.Is(err, ErrConfidence):
|
|
return codeConfidence
|
|
case errors.Is(err, ErrVoidsMissing):
|
|
return codeVoidsMissing
|
|
case errors.Is(err, ErrNudgeNotFound):
|
|
return codeNudgeNotFound
|
|
case errors.Is(err, ErrNudgeOutcome):
|
|
return codeNudgeOutcome
|
|
case errors.Is(err, ErrReminderNotFound):
|
|
return codeReminderMissing
|
|
case errors.Is(err, ErrReminderState):
|
|
return codeReminderState
|
|
case errors.Is(err, ErrToolNotFound):
|
|
return codeToolNotFound
|
|
case errors.Is(err, ErrUnknownMethod):
|
|
return codeUnknownMethod
|
|
case errors.Is(err, ErrBadParams):
|
|
return codeBadParams
|
|
case errors.Is(err, ErrForbidden):
|
|
return codeForbidden
|
|
default:
|
|
return codeInternal
|
|
}
|
|
}
|
|
|
|
// rpcErr builds the wire error for a server-side error. message is omitted
|
|
// for sentinel codes (the Code carries the meaning; no need to echo text the
|
|
// caller can re-derive from errors.Is) and included for internal/bad-params
|
|
// where the text is the actual diagnostic.
|
|
func rpcErr(err error) *RpcError {
|
|
c := codeOf(err)
|
|
if c == codeInternal || c == codeBadParams {
|
|
return &RpcError{Code: c, Message: err.Error()}
|
|
}
|
|
return &RpcError{Code: c}
|
|
}
|