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"` DeliveryGroup string `json:"delivery_group,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"` DeliveryGroup string `json:"delivery_group,omitempty"` DeliveryAttempts int `json:"delivery_attempts,omitempty"` NextAttemptTs time.Time `json:"next_attempt_ts,omitempty"` DeliveryBlockedTs time.Time `json:"delivery_blocked_ts,omitempty"` DeliveryBlockedError string `json:"delivery_blocked_error,omitempty"` } // 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"` // 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:", 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:") 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") )