Skip to main content

meerkat_mobkit/memory/
capabilities.rs

1//! Judgment-plane capability traits (M4 de-weld).
2//!
3//! The full judgment plane — the §10.1 taint firewall controls, the Steward's
4//! dream read/write surface, and the console Memory panel's read API — used
5//! to be inherent methods on the bundled [`SqliteAgentMemoryStore`]; every
6//! consumer (StewardEngine, `memory_wiring`, the console panel, the builder)
7//! was welded to that concrete type. These traits promote that surface so the
8//! plane runs against ANY provider that advertises the capability, with the
9//! bundled store as just one implementor — the same shape the Distiller
10//! (`AgentMemoryProvider` + `TombstoneSource`) and the RecallCoordinator
11//! (`SelectedRecordFetch`) already have.
12//!
13//! Discovery is by capability accessor on [`AgentMemoryProvider`]
14//! (`as_taintable`, `as_steward_store`, `as_memory_panel_store`,
15//! `as_selected_record_fetch`, `as_tombstone_source`), replacing the deleted
16//! `as_sqlite_store()` downcast — the provider trait no longer names its own
17//! implementation.
18//!
19//! Trait cut, deliberately layered:
20//! - [`TaintableStore`] stands alone: firewall wiring happens before any
21//!   engine exists and is required even when no engine is enabled.
22//! - [`StewardStore`] extends [`StagedMemoryStore`] (the dream stages and
23//!   commits batches) and [`TombstoneSource`] (the orient phase renders
24//!   recent tombstones).
25//! - [`MemoryPanelStore`] extends [`StewardStore`]: seven of the panel's
26//!   fifteen reads are steward reads, and everything the panel renders
27//!   (dream runs, promotions, proposals, harvests) is judgment-plane output
28//!   — a provider that can serve the panel can serve the steward. The
29//!   supertrait keeps the shared methods defined exactly once instead of
30//!   duplicating names across two traits on the same implementor.
31//!
32//! All row types here are plain portable data (no SQLite types leak).
33//!
34//! [`SqliteAgentMemoryStore`]: crate::memory::sqlite_store::SqliteAgentMemoryStore
35//! [`AgentMemoryProvider`]: crate::identity_first::agent_memory::AgentMemoryProvider
36//! [`SelectedRecordFetch`]: crate::memory::selector::SelectedRecordFetch
37
38use std::sync::Arc;
39
40use async_trait::async_trait;
41
42use crate::identity_first::agent_memory::AgentMemoryError;
43use crate::memory::distiller::TombstoneSource;
44use crate::memory::events::MemoryEventSink;
45use crate::memory::records::{
46    InjectionLogEntry, MemoryAuthor, MemoryId, MemoryRecord, MemoryScope, NewMemoryRecord,
47    ProposalId,
48};
49use crate::memory::staged::{StageToken, StagedMemoryStore};
50use crate::memory::taint::LlmWriteGate;
51
52// ---------------------------------------------------------------------------
53// Evidence resolution (re-homed from sqlite_store.rs)
54// ---------------------------------------------------------------------------
55
56/// §10.2 P3: whether an [`EvidenceRef`] resolves against the persistent
57/// session store (session exists; a cited range lies within the persisted
58/// transcript). The semantic endorsement half of an `agent_verified` retier
59/// is the dream's judgment (recorded in the op rationale); this is the
60/// mechanical half.
61///
62/// [`EvidenceRef`]: crate::memory::records::EvidenceRef
63pub trait EvidenceRefResolver: Send + Sync {
64    fn resolves(&self, evidence: &crate::memory::records::EvidenceRef) -> Result<(), String>;
65}
66
67// ---------------------------------------------------------------------------
68// Firewall control surface (§10.1)
69// ---------------------------------------------------------------------------
70
71/// The §10.1 taint-firewall control surface: install the LLM write gate, the
72/// evidence-ref resolver, and the timeline event sink on a store. The
73/// `*_if_absent` variants are load-bearing for the classic builder path,
74/// which must never clobber a gate or sink an embedder installed before
75/// handing the store over.
76pub trait TaintableStore: Send + Sync {
77    /// Install the §10.1 LLM write gate. Wiring installs it at startup,
78    /// before any member can dispatch a write.
79    fn set_llm_write_gate(&self, gate: Arc<dyn LlmWriteGate>);
80
81    /// Install the §10.1 gate only when none is present. Returns whether
82    /// this call installed the gate.
83    fn set_llm_write_gate_if_absent(&self, gate: Arc<dyn LlmWriteGate>) -> bool;
84
85    /// Install the §10.2 evidence-ref resolver. The steward wiring installs
86    /// it at startup; from then on every staged retier to `agent_verified`
87    /// must cite evidence that resolves against the session store.
88    fn set_evidence_resolver(&self, resolver: Arc<dyn EvidenceRefResolver>);
89
90    /// Wire the §9.3 timeline sink for quarantined-write events.
91    fn set_event_sink(&self, sink: Arc<dyn MemoryEventSink>);
92
93    /// Wire the §9.3 sink only when none is present. Returns whether this
94    /// call installed the sink.
95    fn set_event_sink_if_absent(&self, sink: Arc<dyn MemoryEventSink>) -> bool;
96}
97
98// ---------------------------------------------------------------------------
99// Steward read/write rows (§8.5)
100// ---------------------------------------------------------------------------
101
102/// Per-scope store overview row for the dream's orient phase (§8.5) and
103/// the P3b console Memory panel.
104#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct ScopeOverview {
106    pub scope: MemoryScope,
107    pub active: u64,
108    pub quarantined: u64,
109    pub superseded: u64,
110    pub tombstoned: u64,
111    pub body_bytes: u64,
112}
113
114/// One pending (or held) mob/operator-scope proposal awaiting a dream
115/// verdict (§8.5 promotion).
116#[derive(Debug, Clone, PartialEq)]
117pub struct PendingProposal {
118    pub proposal_id: ProposalId,
119    pub scope: MemoryScope,
120    pub record: NewMemoryRecord,
121    pub author: MemoryAuthor,
122    pub status: String,
123    pub created_at_ms: u64,
124    /// §10.1 propose-time taint fact: `Some(reason)` when the write gate
125    /// would have quarantined this author at propose time. A plain steward
126    /// "accept" on a tainted proposal downgrades to an operator gate.
127    pub taint: Option<String>,
128}
129
130/// One retired identity awaiting an exit-interview harvest (§8.5).
131#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct PendingHarvest {
133    pub identity: String,
134    pub session_key: Option<String>,
135    pub cause: String,
136    pub retired_at_ms: u64,
137}
138
139/// One gated quarantine-promotion (§10.2): the staged batch commits only on
140/// gating approval.
141#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct PendingPromotion {
143    pub pending_id: String,
144    pub stage_token: String,
145    pub record_id: MemoryId,
146    pub scope_kind: String,
147    pub scope_key: String,
148    pub rationale: Option<String>,
149    pub status: String,
150    pub created_at_ms: u64,
151}
152
153/// One persisted dream run (§8.5): the durable verdict sheet — phases,
154/// verdict counters, and skips as `DreamRun::detail()` JSON — written by the
155/// steward at the end of every pipeline run (one row per partition run).
156#[derive(Debug, Clone, PartialEq, Eq)]
157pub struct PersistedDreamRun {
158    pub run_id: String,
159    pub partition_label: String,
160    pub started_at_ms: u64,
161    pub completed_at_ms: u64,
162    pub ops_committed: u64,
163    /// `DreamRun::detail()` JSON text (phases, verdicts, skips).
164    pub detail: String,
165}
166
167/// One usage-audit verdict awaiting (or holding) operator review — the
168/// "memories you might want to correct" queue (§16 Q6). `resolved_at_ms`
169/// NULL = open.
170#[derive(Debug, Clone, PartialEq, Eq)]
171pub struct DreamAuditVerdict {
172    pub run_id: String,
173    pub record_id: String,
174    pub verdict: String,
175    pub rationale: String,
176    pub created_at_ms: u64,
177    pub resolved_at_ms: Option<u64>,
178    pub resolution: Option<String>,
179}
180
181// ---------------------------------------------------------------------------
182// Steward capability (§8.5)
183// ---------------------------------------------------------------------------
184
185/// The Steward's dream read/write surface (§8.5): orient aggregates, the
186/// proposal/quarantine/harvest queues, gated-promotion bookkeeping, and the
187/// durable dream ledger. [`StagedMemoryStore`] supplies stage/commit (and
188/// through it the provider recall/manifest surface); [`TombstoneSource`]
189/// supplies the orient phase's recent-tombstone render.
190#[async_trait]
191pub trait StewardStore: StagedMemoryStore + TombstoneSource {
192    /// The per-scope retention floors this store warns against (§7.3);
193    /// rendered into the dream's orient overview as floor pressure.
194    fn scope_floors(&self) -> (usize, usize);
195
196    /// Per-scope counts and byte totals for a realm — the orient phase's
197    /// one cheap aggregate.
198    async fn scope_overview(&self, realm: &str) -> Result<Vec<ScopeOverview>, AgentMemoryError>;
199
200    /// Pending/held proposals, oldest first (§8.5 promotion queue).
201    async fn pending_proposals(
202        &self,
203        realm: &str,
204        limit: usize,
205    ) -> Result<Vec<PendingProposal>, AgentMemoryError>;
206
207    /// Record a dream verdict on a proposal: `accepted`, `rejected`, or
208    /// `held` (held stays in the pending queue for the next dream).
209    async fn set_proposal_status(
210        &self,
211        realm: &str,
212        proposal_id: &str,
213        status: &str,
214    ) -> Result<(), AgentMemoryError>;
215
216    /// Quarantined records, newest first — the dream's review queue (§8.5).
217    /// The steward is the one stage that reads these bodies wholesale; the
218    /// caller renders them defanged.
219    async fn quarantined_records(
220        &self,
221        realm: &str,
222        limit: usize,
223    ) -> Result<Vec<MemoryRecord>, AgentMemoryError>;
224
225    /// Records by id, any status — the gather phase's bounded body fetch.
226    /// Missing ids are skipped (the model may cite stale ids).
227    async fn records_by_ids(
228        &self,
229        realm: &str,
230        ids: &[String],
231    ) -> Result<Vec<MemoryRecord>, AgentMemoryError>;
232
233    /// Most recently updated records in a realm, any scope, active or
234    /// quarantined — the gather phase filters (e.g. recent distillates by
235    /// author) host-side.
236    async fn recent_records(
237        &self,
238        realm: &str,
239        limit: usize,
240    ) -> Result<Vec<MemoryRecord>, AgentMemoryError>;
241
242    /// Newest-first injection-ledger rows for a realm (§9.2). Read surface
243    /// for the steward's usage audit and the console Memory panel.
244    async fn injection_log(
245        &self,
246        realm: &str,
247        limit: usize,
248    ) -> Result<Vec<InjectionLogEntry>, AgentMemoryError>;
249
250    /// Record a retired identity for the next dream's exit-interview
251    /// harvest (§8.5). Idempotent per (identity, retired_at_ms).
252    async fn record_pending_harvest(
253        &self,
254        realm: &str,
255        identity: &str,
256        session_key: Option<&str>,
257        cause: &str,
258    ) -> Result<(), AgentMemoryError>;
259
260    /// Pending exit-interview harvests, oldest first.
261    async fn pending_harvests(
262        &self,
263        realm: &str,
264        limit: usize,
265    ) -> Result<Vec<PendingHarvest>, AgentMemoryError>;
266
267    /// Mark one exit-interview harvest done.
268    async fn mark_harvest_complete(
269        &self,
270        realm: &str,
271        identity: &str,
272        retired_at_ms: u64,
273    ) -> Result<(), AgentMemoryError>;
274
275    /// Record a gated quarantine-promotion: gating `pending_id` → staged
276    /// batch token (§10.2). Only a gating approval commits the token.
277    async fn record_pending_promotion(
278        &self,
279        realm: &str,
280        promotion: PendingPromotion,
281    ) -> Result<(), AgentMemoryError>;
282
283    /// Look up a still-pending gated promotion by its gating pending id.
284    async fn pending_promotion_by_id(
285        &self,
286        realm: &str,
287        pending_id: &str,
288    ) -> Result<Option<PendingPromotion>, AgentMemoryError>;
289
290    /// All still-pending gated promotions (dream-start reconciliation).
291    async fn pending_promotions(
292        &self,
293        realm: &str,
294    ) -> Result<Vec<PendingPromotion>, AgentMemoryError>;
295
296    /// Resolve a gated promotion: `committed`, `denied`, or `expired`.
297    async fn resolve_pending_promotion(
298        &self,
299        realm: &str,
300        pending_id: &str,
301        status: &str,
302    ) -> Result<(), AgentMemoryError>;
303
304    /// Re-key a gated promotion after a gating escalation minted a
305    /// successor pending entry.
306    async fn rekey_pending_promotion(
307        &self,
308        realm: &str,
309        old_pending_id: &str,
310        new_pending_id: &str,
311    ) -> Result<(), AgentMemoryError>;
312
313    /// Discard a staged-but-uncommitted batch (denied/expired gated
314    /// promotions; §8.5 crash semantics keep this safe — an unapplied stage
315    /// row is never visible).
316    async fn discard_stage(&self, token: StageToken) -> Result<(), AgentMemoryError>;
317
318    /// Persist one completed dream run (idempotent on run_id).
319    async fn save_dream_run(
320        &self,
321        realm: &str,
322        run: PersistedDreamRun,
323    ) -> Result<(), AgentMemoryError>;
324
325    /// Record the usage-audit verdicts of one dream run. Only non-clean
326    /// verdicts belong here (the review queue); load-bearing records are
327    /// counted in the run detail, not queued.
328    async fn save_dream_audit_verdicts(
329        &self,
330        realm: &str,
331        run_id: &str,
332        verdicts: Vec<(String, String, String)>,
333    ) -> Result<(), AgentMemoryError>;
334}
335
336// ---------------------------------------------------------------------------
337// Console Memory panel rows and capability (§9.3, P3b)
338// ---------------------------------------------------------------------------
339
340/// One page of panel records: strictly-descending `(updated_at_ms,
341/// memory_id)` keyset pagination.
342#[derive(Debug, Clone, PartialEq, Eq)]
343pub struct PanelRecordsPage {
344    pub records: Vec<MemoryRecord>,
345    /// Pass back as `cursor` to continue; `None` when exhausted.
346    pub next_cursor: Option<(u64, String)>,
347}
348
349/// One steward dream run reconstructed from its audit rows (every committed
350/// op records its `Steward { run_id }` author in the audit `detail`).
351#[derive(Debug, Clone, Default, PartialEq, Eq)]
352pub struct DreamRunAudit {
353    pub run_id: String,
354    pub first_op_at_ms: u64,
355    pub last_op_at_ms: u64,
356    pub ops: u64,
357    /// op kind → count (create/supersede/tombstone/retier/set_rank).
358    pub op_kinds: std::collections::BTreeMap<String, u64>,
359    /// Ops that landed quarantined at the write seam.
360    pub quarantined_ops: u64,
361    /// Bounded sample of touched record ids, newest first.
362    pub memory_ids: Vec<String>,
363    /// Bounded sample of op rationales, newest first.
364    pub rationales: Vec<String>,
365}
366
367/// The console Memory panel's read API (§9.3): the ten read-only
368/// `mobkit/memory/panel/*` RPCs. Seven of the panel's reads are steward
369/// reads (overview, proposals, quarantine, promotions, harvests, injection
370/// ledger, scope floors) — the [`StewardStore`] supertrait carries those;
371/// this trait adds the panel-only listing, lineage, and dream-ledger reads.
372/// Everything the panel renders is judgment-plane output, so requiring the
373/// steward surface is the honest capability bar, not an over-ask.
374#[async_trait]
375pub trait MemoryPanelStore: StewardStore {
376    /// Realms with store state (panel realm picker).
377    async fn panel_realms(&self) -> Result<Vec<String>, AgentMemoryError>;
378
379    /// One record by id, any status.
380    async fn record_by_id(
381        &self,
382        realm: &str,
383        memory_id: &str,
384    ) -> Result<Option<MemoryRecord>, AgentMemoryError>;
385
386    /// Panel record listing: optional scope/status filters, newest-updated
387    /// first, keyset cursor. Any status is visible here — the panel is an
388    /// inspection surface and renders status explicitly.
389    async fn records_page(
390        &self,
391        realm: &str,
392        scope_kind: Option<&str>,
393        scope_key: Option<&str>,
394        status_kind: Option<&str>,
395        limit: usize,
396        cursor: Option<(u64, String)>,
397    ) -> Result<PanelRecordsPage, AgentMemoryError>;
398
399    /// Supersede lineage around one record, oldest first: ancestors via the
400    /// `supersedes` pointer, the record itself, then committed successors
401    /// via the `Superseded { by }` status link. When the tip has no
402    /// committed successor, records *claiming* to supersede it (e.g. a
403    /// quarantined supersede that left the prior active, §10.1) are
404    /// appended without recursing — claims are visible but never extend
405    /// the walk. Bounded by `max_len`, cycle-safe.
406    async fn supersede_chain(
407        &self,
408        realm: &str,
409        memory_id: &str,
410        max_len: usize,
411    ) -> Result<Vec<MemoryRecord>, AgentMemoryError>;
412
413    /// Newest-first injection-ledger rows for one record (panel usage view).
414    async fn injection_log_for_record(
415        &self,
416        realm: &str,
417        record_id: &str,
418        limit: usize,
419    ) -> Result<Vec<InjectionLogEntry>, AgentMemoryError>;
420
421    /// Persisted dream runs, newest first.
422    async fn dream_runs(
423        &self,
424        realm: &str,
425        limit: usize,
426    ) -> Result<Vec<PersistedDreamRun>, AgentMemoryError>;
427
428    /// Open (unresolved) audit verdicts, newest first — the operator review
429    /// queue. One row per (run, record); the console dedups by record.
430    async fn open_dream_audit_verdicts(
431        &self,
432        realm: &str,
433        limit: usize,
434    ) -> Result<Vec<DreamAuditVerdict>, AgentMemoryError>;
435
436    /// Dream-run summaries reconstructed from steward audit rows, newest
437    /// run first. Bounded scan; runs older than the scan window fall off
438    /// the panel, which is acceptable for a history summary surface.
439    async fn dream_history(
440        &self,
441        realm: &str,
442        max_runs: usize,
443    ) -> Result<Vec<DreamRunAudit>, AgentMemoryError>;
444}