Files
Maven/internal/ipc/api.go
T
claude 4d97280d74 a turn hands back its trace id, and one wire op corrects it (V-630)
The correction is the only supervised signal in the box, so the cost of
giving one has to be near zero. That means the surface needs the trace id of
the turn it is showing, which it had no way to learn: handleText returns one
string and the trace was written after the reply left.

The id rides back on ChatReply through the same context sink querySource
uses, so the mic, telegram and the web keep the one signature they share.
CorrectTurn takes a trace id and an optional target, which is deliberately
reach-agnostic: nothing about it assumes a browser.

store.ErrNoSuchTrace gets a wire twin. A turn past the retention bound is
gone, and that is the expected outcome of correcting an old turn, not a
broken database.
2026-08-06 19:45:37 +04:00

1006 lines
41 KiB
Go

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"`
}
// EcosystemTrace — one hop of a cross-service ecosystem call, read by the
// monitoring surfaces. Traces live in their own store table, not in facts:
// they are written at machine rate and would otherwise crowd every bounded
// reader of facts.
type EcosystemTrace struct {
ID int64 `json:"id"`
Ts time.Time `json:"ts"`
Service string `json:"service"`
Operation string `json:"operation"`
Status string `json:"status"`
DurationMs int64 `json:"duration_ms"`
CorrelationID string `json:"correlation_id"`
CausationID string `json:"causation_id"`
HTTPStatus int `json:"http_status"`
Fields map[string]any `json:"fields,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"`
}
// DeliveryAttempt — one row of the delivery outbox. Times are formatted by the
// reader; Completed is nil while the attempt is still pending.
type DeliveryAttempt struct {
ID int64 `json:"id"`
Kind string `json:"kind"`
Rule string `json:"rule,omitempty"`
ReminderID int64 `json:"reminder_id,omitempty"`
Channel string `json:"channel"`
Status string `json:"status"`
Created time.Time `json:"created"`
Completed *time.Time `json:"completed,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:<account>". 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"`
// DoneWhen — the acceptance criterion. Empty until he writes one, and a
// candidate with no criterion cannot be promoted to open (Vikunja #510).
DoneWhen string `json:"done_when,omitempty"`
// BlockedOn — a canonical Nexus entity id, never a name.
BlockedOn string `json:"blocked_on,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:<account>", 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"`
// DoneWhen and BlockedOn are optional at intake. A derived source leaves
// both empty: mail says what to do, not what finishing means, and guessing
// a criterion would put Maven's reading in the field he is meant to write.
DoneWhen string `json:"done_when,omitempty"`
BlockedOn string `json:"blocked_on,omitempty"`
}
// 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"`
}
// SeedEventReq — write one fact at a caller-supplied timestamp and run the
// pattern path over it, so a recurring routine can be produced on demand
// instead of over real days (Vikunja #518).
//
// This is the ONLY backdating write path in the tree, and it exists for one
// reason: the detector needs four events spread over hours before it proposes
// anything, so V-43, V-46, V-247 and V-254 could not be verified against a
// running daemon at all. A store fixture would have exercised the detector
// without the wiring those tasks doubt.
//
// Two things hold it shut. It is AuthStepUp in the authority table, the same
// rung as mutating the tool allowlist. And mavend refuses it outright unless
// started with -allow-seed, so a box nobody is testing carries no live
// backdating path even for a caller who cleared the gate.
//
// Key and Value are a fact, not an event: extraction runs for real, so a key
// the extractor ignores seeds nothing and says so. That is deliberate — a
// seam that accepted action and object directly would let QA prove a detector
// against events no utterance could ever produce.
type SeedEventReq struct {
Key string `json:"key"`
Value string `json:"value"`
Ts time.Time `json:"ts"`
}
// SeedEventResp — what the seed produced. Extracted is false when the fact was
// written but yielded no event, which is the extractor declining rather than a
// failure. Proposed is true only when this seed completed a pattern; the first
// three seeds of a run return false with no routine.
type SeedEventResp struct {
FactID int64 `json:"fact_id"`
EventID int64 `json:"event_id,omitempty"`
Extracted bool `json:"extracted"`
Action string `json:"action,omitempty"`
Object string `json:"object,omitempty"`
Proposed bool `json:"proposed"`
RoutineID int64 `json:"routine_id,omitempty"`
// IntervalDays — the median the detector settled on, echoed so QA can
// check it against the spacing it asked for.
IntervalDays float64 `json:"interval_days,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.
//
// Created == 0 && !Skipped therefore means the model WAS consulted and found no
// task, which is the common answer. A reader deciding whether to mark a UID
// seen should treat that the same as a success: asking again would spend the
// resident model on the same negative answer. Skipped means the same for a
// different reason. Only an error means "not read yet".
//
// 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, and core refuses a request carrying both:
// it used to take the ID branch and drop the bytes without a word.
//
// The method exists when core has a media store. Vision being off does NOT
// remove it: the bytes are stored and the answer says she cannot read the
// picture yet, which is re-runnable by ID once a vision model is on disk, and
// it is the state this box is in today. So a surface that gets a reply with an
// id and an empty description has not failed, it has stored something. With no
// media block the method answers ErrUnknownMethod, which is what "off unless
// configured" looks like at the wire.
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:<id-prefix>") so it is recallable later. Default false: a
// glance at a screenshot is not automatically a memory.
//
// Setting it raises what the call needs: an embedded note is recall corpus,
// so the caller's source scope must cover auth.ImageNoteSource. Describing
// without saving stays an ordinary read.
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"`
// Token names THIS session. Every later append, stop and discard has to
// carry it. Without it the recorder is addressed by "whatever is running
// now", and a client whose session already ended on the duration cap goes on
// appending its microphone into the next session someone else started.
Token string `json:"token"`
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 {
// Token from CaptureStartResp. A frame for a session that already ended is
// refused rather than folded into whatever is running now.
Token string `json:"token"`
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 {
// Token from CaptureStartResp. Stopping by "whatever is running" lets a
// late client end a recording it never started.
Token string `json:"token"`
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 the normal shape, not a
// failure: summarising a long meeting is a map-reduce of minutes, so stop
// answers with the words and the summary note is written afterwards. Summary is
// filled in only when it happened to be ready. A response with a BlobID and no
// transcript is the audio surviving a transcription failure — the same id can be
// run again by hand off the blob before media.retention prunes it — there is no
// capture method that takes a blob id, so this is not a re-run the wire offers.
// 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"`
// Damaged — the stored row's metadata did not read back cleanly. The
// voiceprint is still there; the name, sample count or enrolment time is
// not trustworthy. A surface should say so rather than render a corrupt
// row as a profile enrolled from zero samples, which is what a real
// minimal enrolment looks like.
Damaged bool `json:"damaged,omitempty"`
}
// 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.
//
// NoBackend is the other failure and it is not a milder one: the rollback failed
// too, no model is loaded, and every phrasing path is on its template fallback
// with routing on the classifier. It is a separate field from RolledBack because
// the two need opposite words on the page.
type SwapModelResp struct {
Model string `json:"model"`
ModelPath string `json:"model_path"`
BaseURL string `json:"base_url"`
RolledBack bool `json:"rolled_back,omitempty"`
NoBackend bool `json:"no_backend,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"`
}
// PingResp — the answer to MethodPing. Alive is always true (the reply itself
// is the proof); Locked says whether the daemon is still waiting for a passkey
// assertion, which is the one state where a CoreAPI read cannot tell an
// operator anything.
type PingResp struct {
Alive bool `json:"alive"`
Locked bool `json:"locked"`
}
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"`
}
// EntityRef — one canonical entity from Nexus. Maven never mints these: an id
// exists because Nexus resolved a name to it.
//
// Ambiguous is the answer when the name matched more than one entity. It is a
// separate state from "not found" because the surface handles them
// differently: an unknown name may be a typo, and an ambiguous one has to be
// asked about, never picked (ECOSYSTEM-SPEC, and the same rule the mutating
// Hexis path follows).
type EntityRef struct {
ID string `json:"id,omitempty"`
Type string `json:"type,omitempty"`
DisplayName string `json:"display_name,omitempty"`
Ambiguous bool `json:"ambiguous,omitempty"`
Candidates []string `json:"candidates,omitempty"`
}
type resolveEntityReq struct {
Query string `json:"query"`
Types []string `json:"types,omitempty"`
}
type resolveEntityResp struct {
Ref EntityRef `json:"ref"`
}
// editTaskReq — the rewrite of the three fields capture set (Vikunja #509).
// Due nil clears the date, so "no date given" and "remove the date" cannot be
// the same request.
type editTaskReq struct {
ID int64 `json:"id"`
Text string `json:"text"`
Due *time.Time `json:"due,omitempty"`
Weight int `json:"weight,omitempty"`
}
// setTaskFieldsReq — the write for the two board columns. Both are sent every
// time and both may be empty: clearing a blocker is as ordinary as setting one,
// so an omitted field cannot mean "leave it alone" without a second way to say
// "make it empty".
type setTaskFieldsReq struct {
ID int64 `json:"id"`
DoneWhen string `json:"done_when,omitempty"`
BlockedOn string `json:"blocked_on,omitempty"`
}
// idReq — methods keyed by a single id, which is every routine transition.
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"`
}
// deliveryAttemptsReq — the outbox read. Status is empty for every status.
type deliveryAttemptsReq struct {
Status string `json:"status,omitempty"`
N int `json:"n"`
}
type kindNReq struct {
Kind string `json:"kind"`
N int `json:"n"`
}
type sourceNReq struct {
Prefix string `json:"prefix"`
N int `json:"n"`
}
type calendarEventsReq struct {
From time.Time `json:"from"`
To time.Time `json:"to"`
}
type revertReq struct {
Key string `json:"key"`
}
// revertResp — the id of the voiding fact the revert wrote.
type revertResp struct {
NewID int64 `json:"new_id"`
}
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.
//
// Conversation names the thread this utterance belongs to: a mavweb session, a
// telegram chat. It is opaque to the daemon and only has to be stable for one
// conversation and distinct across them. Empty is allowed and means "the
// unattributed text tap", which is what an old client sends.
type chatReq struct {
Text string `json:"text"`
Conversation string `json:"conversation,omitempty"`
}
type chatResp struct {
Reply string `json:"reply"`
Source string `json:"source,omitempty"`
TraceID int64 `json:"trace_id,omitempty"`
}
// correctTurnReq — the owner correcting one persisted turn (V-630).
type correctTurnReq struct {
TraceID int64 `json:"trace_id"`
ShouldBe string `json:"should_be,omitempty"`
}
// ChatReply — one text turn's answer plus which query source claimed it.
//
// Source is diagnostic and is empty unless the turn was a question a source
// claimed: a fact write, an act or a chat turn names none. It exists because
// the claiming source was readable only in the daemon log, so a QA step could
// not tell a wrong answer from a wrongly ordered chain (V-539). It is not
// authorization and nothing routes on it.
type ChatReply struct {
Reply string
Source string
// TraceID is the persisted routing trace for this turn (V-629), and it is
// what makes a correction one gesture: the surface already has the id, so
// saying "that was wrong" costs a button and no lookup. 0 ⇒ nothing was
// persisted, which is a box with no database, and the surface offers no
// correction rather than a broken one.
TraceID int64
}
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"`
}
// 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"`
// NoticedAt — when the journal took it. This is the order the ring returns
// and the one a page must sort and label by; OccurredAt is when the thing
// happened, which for a cold feed read is a week before it arrived.
NoticedAt time.Time `json:"noticed_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"`
}
// --- Turn decision trace DTOs (V-564) ---
// TurnClaim — one claimant's say on one turn: who, at which stage, what it
// would have made the turn, the score it reported if it has one, and what
// happened to the claim. Same shape as RuleTrace above and for the same reason:
// a winner alone does not explain an arbitration, the losers do.
type TurnClaim struct {
Stage string `json:"stage"`
Claimant string `json:"claimant"`
Intent string `json:"intent,omitempty"`
Score float64 `json:"score,omitempty"`
HasScore bool `json:"has_score,omitempty"`
Outcome string `json:"outcome"`
Reason string `json:"reason,omitempty"`
}
// TurnDecision — one turn's arbitration, newest first when read as a list.
type TurnDecision struct {
Ts time.Time `json:"ts"`
Utterance string `json:"utterance"`
Winner string `json:"winner"`
Claims []TurnClaim `json:"claims"`
}
// 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. Called by mavweb after a verified assertion.
//
// 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.
//
// Explicit says the operator asked for the cold-start key to be written, as
// opposed to it being a side effect of asserting a passkey. Without the flag
// the daemon writes only when no blob exists yet. Rewriting on every assertion
// is what let a page-level compromise substitute its own PRF value and have
// the daemon re-wrap the real database key under it, and what let a second
// authenticator silently replace the first one's blob.
type storeEncryptionKeyReq struct {
Secret []byte `json:"secret"`
Explicit bool `json:"explicit,omitempty"`
}
// 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")
// ErrTaskNoDoneWhen — a candidate cannot be promoted to open with no
// definition of done (Vikunja #510). Carried across the wire so the /tasks
// form can say which refusal it hit rather than "не найдено".
var ErrTaskNoDoneWhen = errors.New("ipc: task has no definition of done")
// ErrTaskDuplicate — an edit would collide with another live task's normalised
// text (Vikunja #509). The surface says which row holds it rather than merging.
var ErrTaskDuplicate = errors.New("ipc: another live task already has this text")
// ErrNoEntity — Nexus resolved the name to nothing. Distinct from an outage,
// which surfaces as the transport error: "there is no such person" and "Nexus
// is down" must not read the same to a caller deciding whether to store an id.
var ErrNoEntity = errors.New("ipc: no such entity")
// ErrNoSuchTrace — the turn a correction names is not in routing_traces. Given
// a wire twin because it is the expected outcome of correcting a turn older than
// the retention bound, and "that turn is gone" and "the database is broken" must
// not read the same to the surface offering the gesture.
var ErrNoSuchTrace = errors.New("ipc: no such routing trace")
// ErrTaskResolved — a resolved task is not editable.
var ErrTaskResolved = errors.New("ipc: task is resolved")
// 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")
)