pub enum WriteCommand {
Show 14 variants
Remember {
episode: Episode,
embedding: Embedding,
audit_principal: Option<String>,
reply: Sender<Result<MemoryId>>,
},
RememberBatch {
items: Vec<(Episode, Embedding)>,
audit_principal: Option<String>,
reply: Sender<Result<Vec<MemoryId>>>,
},
Forget {
memory_id: MemoryId,
reason: String,
audit_principal: Option<String>,
reply: Sender<Result<()>>,
},
Update {
memory_id: MemoryId,
content: String,
embedding: Embedding,
audit_principal: Option<String>,
reply: Sender<Result<MemoryUpdateReport>>,
},
IngestDocument {
path: PathBuf,
chunk_config: ChunkConfig,
audit_principal: Option<String>,
reply: Sender<Result<IngestReport>>,
},
ForgetDocument {
doc_id: DocumentId,
audit_principal: Option<String>,
reply: Sender<Result<ForgetDocumentReport>>,
},
Consolidate {
scope: ConsolidationScope,
audit_principal: Option<String>,
reply: Sender<Result<ConsolidationReport>>,
},
Reembed {
scope: ReembedScope,
audit_principal: Option<String>,
reply: Sender<Result<ReembedReport>>,
},
SaveSnapshot {
reply: Sender<Result<()>>,
},
Backup {
dest_path: PathBuf,
reply: Sender<Result<()>>,
},
NormalizeSubjects {
aliases: Vec<(String, String)>,
dry_run: bool,
audit_principal: Option<String>,
reply: Sender<Result<NormalizeReport>>,
},
EmitLlmSamplingAudit {
event: AuditEvent,
reply: Sender<Result<()>>,
},
AttachAbstractionBatch {
items: Vec<(MemoryId, SemanticAbstraction)>,
episode_count: usize,
duration_ms: u64,
clusters_deferred: usize,
audit_principal: Option<String>,
reply: Sender<Result<AttachAbstractionBatchReport>>,
},
ResolveContradiction {
a_id: String,
b_id: String,
kind: String,
status: String,
resolution_note: Option<String>,
winning_triple_id: Option<String>,
audit_principal: Option<String>,
reply: Sender<Result<ResolveContradictionReport>>,
},
}Expand description
All write operations go through this enum. Each variant carries a oneshot reply channel.
v0.8.0 P4: every mutating variant also carries audit_principal: Option<String> — the authenticated principal’s subject. Threaded
from the auth middleware (HTTP / MCP) through to the writer-actor,
where the synchronous audit emit records “who did this”. None
covers CLI / no-auth / system-initiated paths.
Variants§
Remember
Fields
RememberBatch
v0.9.2: atomically insert N episodes in one BEGIN IMMEDIATE tx. Used by agentic clients (solo-jarvis) that write back a full turn — user message + assistant response + tool outputs — as one transactional unit so a session crash can never leave a half- persisted turn.
Same outbox-via-pending_index discipline as single Remember:
BEGIN IMMEDIATE → INSERTs (episodes + embeddings + pending_index
per item) → ONE batch-level audit row inside the tx → COMMIT →
hnsw.add per item → DELETE pending_index rows. If an
hnsw.add crashes mid-batch the SQL state is already committed
and the un-drained outbox rows replay on next startup.
Item count capped at MAX_REMEMBER_BATCH_SIZE; over-cap
requests are rejected before BEGIN with Error::InvalidInput.
Reply is Vec<MemoryId> in input order — caller pairs them with
their input items by position.
Fields
Forget
Update
Fields
reply: Sender<Result<MemoryUpdateReport>>IngestDocument
Ingest a document from path into the documents / document_chunks
tables, embedding each chunk via the writer’s configured Embedder.
Same outbox-via-pending_index discipline as Remember: BEGIN
IMMEDIATE → INSERT documents → INSERT document_chunks → INSERT
pending_index (kind=‘chunk’) → COMMIT → hnsw.add per chunk →
DELETE pending_index rows. Content-hash dedup short-circuits
re-ingest of the same normalized text.
Available only when the writer was spawned with an active embedder
(the spawn_full_with_embedder* variants). Other spawn paths get
a clear “not configured” error — same pattern as Reembed.
ForgetDocument
Soft-delete a document: set documents.status='forgotten' and
tombstone every chunk’s HNSW rowid. Chunks remain in SQL for
forensic value; queries that JOIN through documents filter
status='active'. Forgotten docs survive content-hash dedup —
re-ingesting the same content returns the forgotten doc_id.
Consolidate
Reembed
SaveSnapshot
Backup
Online encrypted backup of the writer’s source database to
dest_path. The destination is created with PRAGMA key bound to
the same raw key the writer holds, so the backup file restores
under the same passphrase + salt as the source.
Available only when the writer was spawned with a KeyMaterial
(the spawn_full_with_key_and_optional_steward variant). Other
spawn paths get a clear “not configured” error.
NormalizeSubjects
Backfill: rewrite historical triples.subject_id and
triples.object_id values per a caller-supplied alias map.
Each (from, to) pair is applied to both the subject and
object columns (a name appearing in either position should
normalize identically).
Opt-in: read-path alias resolution (v0.5.0 P1) already covers
query-time bridging without touching stored rows. This command
is for users who want the underlying data to match the canonical
identity (e.g., when exporting to a system that won’t honor
IdentityConfig.user_aliases). See docs/dev-log/0071-v0.5.x-roadmap.md
Priority 10.
Fields
aliases: Vec<(String, String)>(from_id, to_id) pairs — e.g. [("alex", "user"), ("bob", "user")]. Each pair is applied as
UPDATE triples SET subject_id = to WHERE subject_id = from
and the symmetric object update.
dry_run: boolWhen true, run the UPDATEs inside a transaction, count the
affected rows, then ROLLBACK instead of committing. The
returned report’s row counts reflect what would have been
rewritten.
reply: Sender<Result<NormalizeReport>>EmitLlmSamplingAudit
v0.9.0 P2: emit a single AuditOperation::LlmSamplingCall row
into the per-tenant audit_events table. Used by
SamplingLlmClient (in solo-api) on every
peer.create_message completion — success or failure.
Routed through the writer-actor so the INSERT lands inside a
dedicated sync transaction on the writer’s connection (lesson
#30: ACID for the sampling call’s only persisted trace). The
reply channel surfaces insert failures to the caller so a
missed audit row can NOT be silently swallowed — the caller
of SamplingLlmClient::complete() must see the failure
because this row is the ONLY record of the call.
Privacy invariant: event.details must NOT contain the
raw prompt content. Enforcement lives at the construction
site (SamplingLlmClient::audit_event); we surface only
metadata (model hint, message count, max_tokens, duration_ms,
total prompt char count). The audit test
sampling_audit_row_omits_raw_prompt_text pins this.
AttachAbstractionBatch
v0.9.0 P4c: persist a batch of (cluster_id, abstraction)
pairs in a single transaction + emit ONE
AuditOperation::MemoryTriplesExtract audit row carrying the
batch’s aggregate counts.
Sent by the daemon-side consolidate-timer’s triples batch
path (see crates/solo-cli/src/commands/daemon.rs:: triples_batch_tick). For each (cluster_id, abstraction):
- INSERT one
semantic_abstractionsrow. - INSERT N
triplesrows (where N =abstraction.triples.len()).
Then emit ONE audit row per batch, with
details_json = {episode_count, cluster_count, abstractions_built, triples_extracted, duration_ms}.
Atomicity (plan §4 P4c / lesson #30): the entire batch
runs inside ONE BEGIN IMMEDIATE tx; the audit emit is
SYNC inside that same tx. If the audit emit fails, the
whole batch aborts — preserving the “audit row IS the only
persisted record of the batch” invariant.
Partial-batch tolerance: per-cluster INSERT failures
(e.g. FK violation if the cluster_id was dropped between
snapshot and persist) are LOGGED and skipped, but the tx
stays open. The audit row’s details_json.abstractions_built
counter reflects the SUCCESSFUL inserts only. Test:
[tests::p4c_attach_abstraction_batch_tests].
Fields
items: Vec<(MemoryId, SemanticAbstraction)>(cluster_id, abstraction) pairs to persist. The
abstraction’s cluster_id field is REQUIRED to equal the
tuple’s MemoryId; the handler asserts this and rejects
the whole batch on mismatch.
episode_count: usizeNumber of episodes that flowed into this batch (for the
audit row’s details_json.episode_count).
duration_ms: u64Wall-time the upstream collection+LLM round-trip took
(for the audit row’s details_json.duration_ms). The
writer-actor’s own tx duration is small and not included.
clusters_deferred: usizev0.10.1 (P4 audit m5): number of clusters that timed out
during their per-cluster abstract_cluster call. Surfaces
in the audit row’s details_json.clusters_deferred and in
the returned AttachAbstractionBatchReport.clusters_deferred.
Distinct from clusters_failed (per-cluster INSERT
SAVEPOINT rollbacks): a deferred cluster never made it INTO
the batch’s items because the LLM call timed out
upstream.
audit_principal: Option<String>Cached audit principal_subject for the daemon path —
usually None since the consolidate timer runs without
an explicit principal.
reply: Sender<Result<AttachAbstractionBatchReport>>ResolveContradiction
Mark a contradiction resolved / unresolved / reopened. Routes
through the writer actor (dev-log 0152 finding H1 — restores
ADR-0003: the previous code path used the reader pool to
UPDATE contradictions, racing with the writer-actor on multiple
connections and writing the audit row outside the tx).
Atomicity: UPDATE + audit emit inside one BEGIN IMMEDIATE transaction. If the audit row insert fails, the UPDATE rolls back.
Trait Implementations§
Auto Trait Implementations§
impl Freeze for WriteCommand
impl !RefUnwindSafe for WriteCommand
impl Send for WriteCommand
impl Sync for WriteCommand
impl Unpin for WriteCommand
impl UnsafeUnpin for WriteCommand
impl !UnwindSafe for WriteCommand
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
Source§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more