package ipc import ( "context" "errors" "time" "github.com/kami/maven/internal/audio" ) // DTOs — wire-level data. Decoupled from internal/store so the protocol is // self-describing and a module never needs to import store internals (the // boundary is the point). The store adapter maps store.* ⇔ these 1:1. // Fact — one observation. Ts is valid-time (true-as-of), as in store. type Fact struct { ID int64 `json:"id"` Ts time.Time `json:"ts"` Kind string `json:"kind"` // "self" | "env" | "config" Key string `json:"key"` Value string `json:"value"` // raw json if structured Source string `json:"source"` // tap:*|infer:*|poll:*|ambient|promote|feedback Confidence float64 `json:"confidence"` VoidsID *int64 `json:"voids_id,omitempty"` } // Bucket — presence hysteresis state: "present" | "away". type Bucket string const ( Present Bucket = "present" Away Bucket = "away" ) // Nudge — one proactive send + its outcome, for the monitoring read path. type Nudge struct { ID int64 `json:"id"` Ts time.Time `json:"ts"` Rule string `json:"rule"` Channel string `json:"channel"` Message string `json:"message"` Outcome string `json:"outcome"` // pending|acted|snoozed|ignored OutcomeTs *int64 `json:"outcome_ts,omitempty"` } // Note — a recall/preference item; ranked by embedding cosine on query. // Score is set by QueryNotes (0 on the write path). type Note struct { ID int64 `json:"id"` Ts time.Time `json:"ts"` Text string `json:"text"` Source string `json:"source"` Score float64 `json:"score"` } // Reminder — user-stated future intent; fires once or recurring (if cron set). type Reminder struct { ID int64 `json:"id"` CreatedTs time.Time `json:"created_ts"` FireTs time.Time `json:"fire_ts"` NextFireTs time.Time `json:"next_fire_ts"` Payload string `json:"payload"` Status string `json:"status"` // pending|fired|cancelled Cron string `json:"cron"` } // Presence — the read the phraser / delivery modules need to decide channel // routing and tone. presence = reachability, NOT wakefulness (spec). Loop // reads probes + computes score itself; modules get the resolved snapshot. type Presence struct { Bucket Bucket `json:"bucket"` Score float64 `json:"score"` Updated time.Time `json:"updated"` } // WriteFactReq — the only state mutation a capture/tool module performs. // Confidence is 1.0 for taps, (0,1) for inferences; the store enforces range. // Source is provenance — the server-side source-scope seam (auth layer) will // refuse a module writing under a source it doesn't own ("compromised poller // can't forge a trigger"). Today the floor permits any local caller. type WriteFactReq struct { Ts time.Time `json:"ts"` Kind string `json:"kind"` Key string `json:"key"` Value string `json:"value"` Source string `json:"source"` Confidence float64 `json:"confidence"` VoidsID *int64 `json:"voids_id,omitempty"` // Subject — free-text "who/what this fact is about" (e.g. "the espresso // machine", "Kate"). Empty (the default, so old callers are unaffected) // means the fact isn't about a resolvable entity. When set, the fact // enrichment worker (cmd/mavend/factenrichment.go) later resolves it // against Nexus into an entity_id — see Vikunja #279. Subject string `json:"subject,omitempty"` } // Task — one captured piece of work (Vikunja #130). Status is // "candidate" (Maven derived it and it is unconfirmed), "open" (his work), // "done" or "dropped". Source is provenance in the facts vocabulary: // "tap:voice", "tap:web", "email:". Evidence is the trail a derived // task came from, empty for anything he stated himself. type Task struct { ID int64 `json:"id"` CreatedTs time.Time `json:"created_ts"` Text string `json:"text"` Source string `json:"source"` Evidence string `json:"evidence,omitempty"` ExternalID string `json:"external_id,omitempty"` Status string `json:"status"` Due *time.Time `json:"due,omitempty"` Weight int `json:"weight,omitempty"` Resolved *time.Time `json:"resolved,omitempty"` ResolvedBy string `json:"resolved_by,omitempty"` } // CaptureTaskReq — THE INTAKE SEAM. Everything that captures a task goes // through this one shape: the voice path, the web form, and (Vikunja #246) the // email reader, which has not been built yet. // // An extractor that reads mail sets Source "email:", Status // "candidate", and Evidence to whatever makes the task reviewable (the subject // line). It cannot set Status "open" — work Maven inferred from something she // read is a suggestion until the owner confirms it on the /tasks page, and the // store refuses an open capture from a derived source rather than trusting the // caller to have read this paragraph. // // ExternalID is what makes re-reading free for such a source, and it is // REQUIRED of one. Text dedupe only covers live rows, because a voice capture // of the same errand next week is a new task. A mailbox has no such signal: it // hands back the same immutable message forever, so a task he already finished // would come back as a fresh candidate on the next poll. ExternalID is unique // over every row whatever its status: message id plus the extracted span. type CaptureTaskReq struct { Text string `json:"text"` Source string `json:"source"` Evidence string `json:"evidence,omitempty"` ExternalID string `json:"external_id,omitempty"` Status string `json:"status,omitempty"` // "" ⇒ open Due *time.Time `json:"due,omitempty"` Weight int `json:"weight,omitempty"` Ts time.Time `json:"ts"` } // CaptureTaskResp — Created is false when the same live task already existed, // in which case ID is the existing row. A caller tells the owner "уже в // списке" rather than claiming it saved something new. type CaptureTaskResp struct { ID int64 `json:"id"` Created bool `json:"created"` // Promoted — this capture turned an existing candidate into open work. He // stated out loud something Maven had only proposed, which is a // confirmation, and the caller says so rather than "уже в списке". Promoted bool `json:"promoted,omitempty"` } // IngestMailReq — one message a mail reader has fetched, handed to core for // extraction (Vikunja #246). // // The mail reader (cmd/mavmaild) holds the IMAP credential and core never sees // it, the same split mavpoll uses for the zenmoney token. What crosses this // boundary is only the message text, because extraction runs on the resident // model and llama-server lives inside core's process. // // Body is already plaintext and truncated by internal/email; core does not // re-parse MIME and never stores the body. Junk means the reader's header // filter already classified the message as bulk — core is told rather than // asked, so a junk message can be counted without a model call. // // This method is available only when core has an email block configured AND a // llama-server phraser; otherwise it answers ErrUnknownMethod, which is what // "off unless configured" looks like at the wire. type IngestMailReq struct { Mailbox string `json:"mailbox"` UID uint32 `json:"uid"` From string `json:"from,omitempty"` Subject string `json:"subject,omitempty"` Date string `json:"date,omitempty"` Body string `json:"body,omitempty"` Junk bool `json:"junk,omitempty"` } // IngestMailResp — what core did with the message. TaskIDs are the rows // CaptureTask returned; Created counts the ones that were new (a re-read // mailbox dedupes to Created=0). Skipped is set when nothing was asked of the // model at all — junk, or an empty message. // // Nothing here echoes the mail back. The reader logs counts. type IngestMailResp struct { TaskIDs []int64 `json:"task_ids,omitempty"` Created int `json:"created"` Skipped bool `json:"skipped,omitempty"` } // DescribeImageReq — one image handed to core to look at (Vikunja #252). // // Data is the raw image file as received (png / jpeg / gif). Core sniffs it and // refuses anything else; a declared content type is not part of this request // because the sender's claim about its own bytes is not evidence. Base64 on the // wire via the usual JSON marshal of []byte. // // Question is what he asked about the picture ("что тут написано?"). Empty ⇒ // core uses its configured default prompt. // // Source is provenance recorded on the stored blob: "telegram", "web:upload". // // Exactly one of Data or ID is set. ID re-describes an image core already has — // a different question, or the first attempt that succeeds after a vision model // finally lands on disk. // // The method exists only when core has both a media store and an enabled vision // block; otherwise it answers ErrUnknownMethod, which is what "off unless // configured" looks like at the wire. A surface cannot make Maven look at // pictures by merely sending one. type DescribeImageReq struct { Data []byte `json:"data,omitempty"` ID string `json:"id,omitempty"` Source string `json:"source,omitempty"` Question string `json:"question,omitempty"` // SaveNote — also write the description as a note (source // "media:image:") so it is recallable later. Default false: a // glance at a screenshot is not automatically a memory. SaveNote bool `json:"save_note,omitempty"` } // DescribeImageResp — what she saw. ID is the stored blob's content address, and // it is set even when Description is empty because the description failed: the // bytes are on disk and the same id can be retried. NoteID is non-zero only when // SaveNote was set and the write succeeded. // // The image itself is never echoed back. type DescribeImageResp struct { ID string `json:"id"` Description string `json:"description,omitempty"` Width int `json:"width,omitempty"` Height int `json:"height,omitempty"` NoteID int64 `json:"note_id,omitempty"` } // CaptureStartReq — begin recording a meeting (Vikunja #253). // // Label is what the meeting is called ("встреча с подрядчиком"); it goes into // the summary note so the note is findable later. Empty is allowed. // // There is no "auto", no keyword and no schedule in this request, and there will // not be: the only way audio enters the recorder is a client that was told to // start, appending frames it was told to append. All four capture methods answer // ErrUnknownMethod unless the operator enabled a capture block, so a surface // cannot start a recording by asking nicely. type CaptureStartReq struct { Label string `json:"label,omitempty"` } // CaptureStartResp — the session that opened. MaxSeconds is the hard cap after // which it stops itself; the caller tells him, so a forgotten recording is his // own informed choice rather than a surprise. type CaptureStartResp struct { Label string `json:"label,omitempty"` Started time.Time `json:"started"` MaxSeconds int `json:"max_seconds"` } // CaptureAppendReq — one chunk of audio for the running session. Refused with // "nothing is being recorded" when no session is open, which is the guard that // makes an ambient path impossible: audio arriving at an idle core is dropped on // the floor, not buffered "just in case". type CaptureAppendReq struct { Audio audio.Audio `json:"audio"` } // CaptureAppendResp — how much has been collected, so a client can show a timer // and notice the cap coming. Expired means the session hit its limit and closed; // stop sending and call capture_stop, the audio so far is kept. type CaptureAppendResp struct { Seconds float64 `json:"seconds"` Expired bool `json:"expired,omitempty"` } // CaptureStopReq — end the running session. // // Discard throws the recording away without transcribing, storing or // summarising anything. This is what "забудь, не записывай" maps to, and it is a // flag rather than a separate method so the client that says "stop" and the // client that says "stop and forget" take the same path to the same session. type CaptureStopReq struct { Discard bool `json:"discard,omitempty"` } // CaptureStopResp — the finished capture. BlobID is the stored WAV, kept under // media.retention like any other blob and pruned with it. // // A response with a Transcript and an empty Summary is a degraded success: the // words exist, only the model failed. A response with a BlobID and neither is // the audio surviving a transcription failure — the same id can be run again. // Discarded is true when nothing was kept. type CaptureStopResp struct { BlobID string `json:"blob_id,omitempty"` Label string `json:"label,omitempty"` Started time.Time `json:"started,omitempty"` Seconds float64 `json:"seconds,omitempty"` Transcript string `json:"transcript,omitempty"` Summary string `json:"summary,omitempty"` Chunks int `json:"chunks,omitempty"` NoteID int64 `json:"note_id,omitempty"` Discarded bool `json:"discarded,omitempty"` } // CaptureStatusResp — what "что ты записываешь?" needs, and what /dash shows. // Running=false with everything else empty is the normal state. type CaptureStatusResp struct { Running bool `json:"running"` Label string `json:"label,omitempty"` Started time.Time `json:"started,omitempty"` Seconds float64 `json:"seconds,omitempty"` Bytes int `json:"bytes,omitempty"` } // EnrollSpeakerReq — register a voice (Vikunja #255). // // Samples are separate utterances recorded deliberately for this purpose, not // audio harvested from ordinary turns. internal/speaker requires several of // them totalling enough seconds, and refuses one long clip: a profile built // from a single sentence encodes that sentence as much as the person. // // There is no "enrol whoever just spoke" request shape, and that omission is // the point. Taking a biometric of a guest because they walked past the // microphone is not something a wire protocol should make easy. type EnrollSpeakerReq struct { ID string `json:"id"` Name string `json:"name,omitempty"` Samples []audio.Audio `json:"samples"` } // Speaker — one enrolled voice as a surface sees it. The voiceprint itself is // never sent: a listing says who is enrolled, it does not hand out the // biometric. type Speaker struct { ID string `json:"id"` Name string `json:"name"` Enrolled time.Time `json:"enrolled"` Samples int `json:"samples"` } // EnrollSpeakerResp — the profile that was written. type EnrollSpeakerResp struct { Speaker Speaker `json:"speaker"` } // ListSpeakersResp — who is enrolled, sorted by id. Enabled is false when no // embedding model is wired, which is this box's state: the profiles can be // listed and deleted, nothing can be recognised. type ListSpeakersResp struct { Speakers []Speaker `json:"speakers"` Enabled bool `json:"enabled"` } // ForgetSpeakerReq — delete one voiceprint. This is the request that must // always work; a biometric someone asked to be rid of has to actually go. type ForgetSpeakerReq struct { ID string `json:"id"` } // SwapModelReq — load another resident model without restarting the daemon // (Vikunja #250). ModelPath must be one of the paths in phraser.swap_models; // anything else is ErrForbidden, and an unconfigured allowlist makes the whole // method ErrUnknownMethod. // // NGpuLayers and NCtx are zero for "keep what is loaded now", which is the // normal case — the same laptop iGPU, a different gguf. // // This is an owner action. It is AuthStepUp in the authority table, it is not on // CoreAPI, and no act, intent or timer can reach it: swapping the model is not // something Maven does to herself. type SwapModelReq struct { ModelPath string `json:"model_path"` NGpuLayers int `json:"n_gpu_layers,omitempty"` NCtx int `json:"n_ctx,omitempty"` } // SwapModelResp — what the daemon ended up serving. Model is the identity the // new llama-server reported for itself, not an echo of the request: if the file // was not the model the operator thought it was, this is where it shows. // // RolledBack is true when the requested model failed to load or would not answer // and the previous one was put back. In that case the call also returns an error // — the swap did not happen — and Model names the model still serving. type SwapModelResp struct { Model string `json:"model"` ModelPath string `json:"model_path"` BaseURL string `json:"base_url"` RolledBack bool `json:"rolled_back,omitempty"` TookMs int64 `json:"took_ms"` } // ModelStatusResp — which model is resident and which ones may be swapped in. // Read-only; the authed page renders it. Swappable is the configured allowlist, // so an empty list means the capability is off. type ModelStatusResp struct { Model string `json:"model"` ModelPath string `json:"model_path"` BaseURL string `json:"base_url"` NGpuLayers int `json:"n_gpu_layers"` NCtx int `json:"n_ctx"` Swappable []string `json:"swappable,omitempty"` } type listTasksReq struct { Status string `json:"status"` // "" all | "live" | candidate|open|done|dropped } type listTasksResp struct { Tasks []Task `json:"tasks"` } type setTaskStatusReq struct { ID int64 `json:"id"` Status string `json:"status"` Ts time.Time `json:"ts"` // By — the caller making the move, in the source vocabulary. Recorded on // the row so a resolved task says what resolved it. By string `json:"by,omitempty"` } // idReq — methods keyed by a single id. type idReq struct { ID int64 `json:"id"` } // markReminderReq — pending→fired|cancelled. type markReminderReq struct { ID int64 `json:"id"` Status string `json:"status"` } // resolveNudgeReq — pending→acted|snoozed|ignored, once. type resolveNudgeReq struct { ID int64 `json:"id"` Outcome string `json:"outcome"` Ts time.Time `json:"ts"` } // keyReq / keySourceReq / sinceReq / outcomesReq — read param shapes. type keyReq struct { Key string `json:"key"` } type keySourceReq struct { Key string `json:"key"` Source string `json:"source"` } type sinceReq struct { Key string `json:"key"` Now time.Time `json:"now"` } type outcomesReq struct { Rule string `json:"rule"` N int `json:"n"` } type nReq struct { N int `json:"n"` } type kindNReq struct { Kind string `json:"kind"` N int `json:"n"` } type calendarEventsReq struct { From time.Time `json:"from"` To time.Time `json:"to"` } type revertReq struct { Key string `json:"key"` } type writeNoteReq struct { Ts time.Time `json:"ts"` Text string `json:"text"` Embedding []float32 `json:"embedding"` Source string `json:"source"` } type queryNotesReq struct { Embedding []float32 `json:"embedding"` K int `json:"k"` } type createReminderReq struct { Fire time.Time `json:"fire"` Payload string `json:"payload"` Cron string `json:"cron"` } type recordNudgeReq struct { Rule string `json:"rule"` Channel string `json:"channel"` Message string `json:"message"` Ts time.Time `json:"ts"` } // idResp / sinceResp — small scalar return wrappers. type idResp struct { ID int64 `json:"id"` } type sinceResp struct { Dur time.Duration `json:"dur"` } // Tool — an act allowlist entry as core exposes it. Status 'proposed' is an // inert scaffold; 'enabled' is runnable. The executor only runs 'enabled'. type Tool struct { Name string `json:"name"` Scope string `json:"scope"` Cmd []string `json:"cmd"` Destructive bool `json:"destructive"` Status string `json:"status"` Utterance string `json:"utterance"` Created time.Time `json:"created"` Updated time.Time `json:"updated"` } // MCPServerStatus — one configured MCP server, as the web surface sees it. // Target is the command or url; Tools is how many tools discovery kept after // allow_tools / max_tools, not how many the server offers. type MCPServerStatus struct { Name string `json:"name"` Transport string `json:"transport"` // "stdio" (a local subprocess) or "http" Target string `json:"target"` Connected bool `json:"connected"` Server string `json:"server,omitempty"` // the server's own name + version Tools int `json:"tools"` Err string `json:"err,omitempty"` } // chatReq / chatResp — text chat round-trip for the IPC Chat method. type chatReq struct { Text string `json:"text"` } type chatResp struct { Reply string `json:"reply"` } type proposeToolReq struct { Name string `json:"name"` Scope string `json:"scope"` Utterance string `json:"utterance"` Ts time.Time `json:"ts"` } type proposeToolResp struct { Proposed bool `json:"proposed"` } type enableToolReq struct { Name string `json:"name"` Scope string `json:"scope"` Cmd []string `json:"cmd"` Destructive bool `json:"destructive"` Ts time.Time `json:"ts"` } type disableToolReq struct { Name string `json:"name"` } type lookupToolReq struct { Name string `json:"name"` } type listToolsReq struct { Status string `json:"status"` } type listToolsResp struct { Tools []Tool `json:"tools"` } // ProposedRoutine — a detected pattern awaiting human confirmation. type ProposedRoutine struct { ID int64 `json:"id"` Action string `json:"action"` Object string `json:"object"` IntervalDays float64 `json:"interval_days"` Status string `json:"status"` // proposed | accepted | dismissed CreatedTs int64 `json:"created_ts"` ReminderID *int64 `json:"reminder_id,omitempty"` } type listProposedRoutinesResp struct { Routines []ProposedRoutine `json:"routines"` } type dismissProposedRoutineReq struct { ID int64 `json:"id"` } type acceptProposedRoutineReq struct { ID int64 `json:"id"` } // CoreAPI — what core exposes to modules. One Go interface, satisfied by: // - the in-process store adapter (server.go storeAPI) — 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 will insert into without // touching the module code. type CoreAPI 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) 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) 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) 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) RecentNudges(ctx context.Context, n int) ([]Nudge, error) 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) // 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. 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) RevertFact(ctx context.Context, key string) (int64, error) // 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 // 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 // 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) // MorningStatus returns each configured morning routine's current // checklist state (see internal/morning): active today/now, which items // are done, which are still missing. The store adapter returns an error // (morning routines are daemon-config, not persisted) — same shape as // TickTrace. MorningStatus(ctx context.Context) ([]MorningRoutineStatus, 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) // 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. // The store adapter returns an error (the plan needs the daemon's routine // config) — same shape as TickTrace and MorningStatus. DayPlan(ctx context.Context) (DayPlan, 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). Chat(ctx context.Context, text string) (string, 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) } // IntakeEvent — one entry of the unified intake journal on the wire. Mirrors // event.Event field for field; the ipc package does not import internal/event // so the wire shape stays independent of the in-process type. type IntakeEvent struct { Source string `json:"source"` Kind string `json:"kind"` EntityIDs []string `json:"entity_ids,omitempty"` Title string `json:"title"` Body string `json:"body,omitempty"` Priority string `json:"priority"` OccurredAt time.Time `json:"occurred_at"` } // --- Rule trace / explanation DTOs --- // RuleTrace — per-rule evaluation result for one tick. type RuleTrace struct { RuleName string `json:"rule_name"` Severity int `json:"severity"` PredicateResult bool `json:"predicate_result"` GateResult bool `json:"gate_result"` GateBlockedBy string `json:"gate_blocked_by,omitempty"` GateDetail GateDetail `json:"gate_detail,omitempty"` WasSelected bool `json:"was_selected"` LostTo string `json:"lost_to,omitempty"` } // GateDetail — snapshot of the values the gate checked. type GateDetail struct { SnoozeUntil *time.Time `json:"snooze_until,omitempty"` CooldownUntil *time.Time `json:"cooldown_until,omitempty"` QuietHours bool `json:"quiet_hours"` CalendarBusy bool `json:"calendar_busy"` Presence string `json:"presence"` InertKeysMissing []string `json:"inert_keys_missing,omitempty"` } // TickTrace — snapshot of one tick's rule evaluations. type TickTrace struct { Now time.Time `json:"now"` Winner string `json:"winner"` Rules []RuleTrace `json:"rules"` } // MorningRoutineItem — one checklist entry's current state. type MorningRoutineItem struct { Key string `json:"key"` Label string `json:"label"` Done bool `json:"done"` } // MorningRoutineStatus — one routine's checklist state right now. type MorningRoutineStatus struct { Name string `json:"name"` Active bool `json:"active"` WindowStart string `json:"window_start"` WindowEnd string `json:"window_end"` Items []MorningRoutineItem `json:"items"` } // DayPlanItem — one line of the day plan. Kind is "event", "reminder" or // "checklist"; Uncertain marks an item whose provenance is below a full // calendar read (a meeting relayed off a phone notification), so a UI can hedge // the same way the spoken form does. type DayPlanItem struct { At time.Time `json:"at"` Text string `json:"text"` Kind string `json:"kind"` Uncertain bool `json:"uncertain,omitempty"` } // DayPlan — the plan for one calendar day. Spoken is the RU sentence maven // says when asked, rendered core-side so the voice reply and the web view can // never drift apart. type DayPlan struct { Date time.Time `json:"date"` Items []DayPlanItem `json:"items"` Spoken string `json:"spoken"` } // storeEncryptionKeyReq — the passkey-derived secret used to wrap the store // encryption key at enrollment time. Called by mavweb after RegisterFinish. // // Secret is the 32-byte WebAuthn PRF output, NOT the credential public key. // The field used to carry the public key and that was the bug: a public key // sits in passkeys.json next to the wrapped blob, so the blob protected // nothing. See internal/webauthn/keywrap.go. type storeEncryptionKeyReq struct { Secret []byte `json:"secret"` } // unlockReq — the passkey-derived secret for unwrapping the store encryption // key at cold-start. mavend reads the wrapped blob from its own configured // path; this is the other half. Same PRF-output contract as above. type unlockReq struct { Secret []byte `json:"secret"` } // ErrToolNotFound — no tool row with this name (re-exported store sentinel for // wire round-tripping via errors.Is). var ErrToolNotFound = errors.New("ipc: tool not found") // callerKey — context key for the authenticated caller. Server sets it from // SO_PEERCRED before dispatch; in-process callers omit it (the adapter treats // a missing Caller as "trusted same-process", the equivalent of the socket's // 0600 floor). type callerKey struct{} // Caller — the peer identity as core sees it. Uid/Pid come from SO_PEERCRED // on Linux; the future auth layer maps Uid + module enrollment → authority. // Today only Uid is populated and used for a same-user check. type Caller struct { Uid int32 Pid int32 } // WithCaller returns ctx annotated with c. Server-side use only. func WithCaller(ctx context.Context, c Caller) context.Context { return context.WithValue(ctx, callerKey{}, c) } // CallerFrom retrieves the Caller, or ok=false if absent (in-process path). func CallerFrom(ctx context.Context) (Caller, bool) { c, ok := ctx.Value(callerKey{}).(Caller) return c, ok } // Sentinel errors. Mirror store's 1:1 so module code reads the same whether // in-process or over the wire. The store adapter translates store.* → these. var ( ErrNoFact = errors.New("ipc: no fact for key") ErrConfidence = errors.New("ipc: confidence must be in (0.0, 1.0]") ErrVoidsMissing = errors.New("ipc: voids_id does not reference an existing fact") ErrNudgeNotFound = errors.New("ipc: nudge not found") ErrNudgeOutcome = errors.New("ipc: nudge already resolved") ErrReminderNotFound = errors.New("ipc: reminder not found") ErrReminderState = errors.New("ipc: reminder not in a mutable state") ErrUnknownMethod = errors.New("ipc: unknown method") ErrBadParams = errors.New("ipc: bad params") // ErrForbidden — the caller's authority doesn't cover this call. The // auth layer's only wire-exported verdict: surface caps the layer, or a // write was out-of-scope, or step-up was required but not asserted. The // text ErrForbidden carries is derived during dispatch (from auth.ErrForbidden // via fmt.Errorf %w wrapping); the wire carries codeForbidden. ErrForbidden = errors.New("ipc: forbidden") )