Skip to main content

ConversationStore

Struct ConversationStore 

Source
pub struct ConversationStore { /* private fields */ }
Expand description

SQLite-backed conversation store (see module docs).

Cheap to clone: clones share one connection behind a mutex. All methods take &self, matching the JSON-backed predecessor.

Implementations§

Source§

impl ConversationStore

Source

pub fn new( root: impl AsRef<Path>, workspace: impl AsRef<Path>, max_per_workspace: usize, ) -> Result<Self>

Open (creating if needed) the store at <root>/conversations.db, scoped to workspace. max_per_workspace is the create-time prune cap (0 = no pruning), identical to the JSON backend.

Source

pub fn workspace_id_for_path(path: impl AsRef<Path>) -> Result<String>

👎Deprecated since 0.6.8:

v1 keying is path-fragile; use newt_core::workspace_key_v2 (17.2). This stays only for the UUIDv5→v2 row migration and legacy-import dir names.

The RETIRED v1 workspace key: UUIDv5 of the canonical path — the derivation the JSON backend used (its per-workspace dir names) and 17.1a inherited for workspace_key. Kept for exactly two lookups: the one-time legacy JSON import (dir names are UUIDv5) and the 17.2 open-time migration that re-keys this workspace’s old rows to crate::workspace_key::workspace_key_v2. Do not key anything new with it.

Source

pub fn wal_fallback_notice(&self) -> Option<&str>

Some(error text) when the database refused WAL and the store is running on the journal_mode=DELETE fallback (typical for NFS homes). Callers should surface this once to the user.

Source

pub fn writer_fingerprint(&self) -> &str

This install’s writer fingerprint — the writer_fingerprint half of the §6 (writer_fingerprint, seq) ordering key.

Source

pub fn create(&self, title: &str, persona: Option<&str>) -> Result<String>

Create a conversation with a freshly minted id; returns the id.

Source

pub fn create_with_id( &self, id: &str, title: &str, persona: Option<&str>, ) -> Result<()>

Create a conversation record using a caller-supplied id.

The TUI pre-generates a conversation id at session start (so the per-session plan path is stable from turn 1, see issue #220) and the record adopts that id when the first turn is saved — same lazy-create contract as the JSON backend.

Source

pub fn exists(&self, id: &str) -> Result<bool>

true if a record for exactly id exists in this workspace. Used by the save path to decide between create_with_id (first turn) and append_turn.

Errors propagate rather than read as “absent”: a transient failure (e.g. a busy reader past the timeout under the NFS DELETE fallback) mistaken for “doesn’t exist” would route the caller into create_with_id and overwrite a live conversation.

Source

pub fn append_turn(&self, id: &str, user: &str, assistant: &str) -> Result<()>

Append one (user, assistant) turn with no tool events and no token usage. id may be a unique prefix. Thin wrapper over append_turn_full: an empty event slice serializes to '[]' and absent tokens to NULL — byte-identical to the pre-17.6 row shape, so existing callers are unchanged.

Source

pub fn append_turn_full( &self, id: &str, user: &str, assistant: &str, events: &[ToolEvent], phantom_reaches: &[PhantomReach], tokens_in: Option<u32>, tokens_out: Option<u32>, ) -> Result<()>

Append one turn with its recorded tool events and backend-reported token usage (Step 17.6, issue #246). id may be a unique prefix.

One BEGIN IMMEDIATE transaction covers: tick allocation, chain extension (prev_hash from the current per-writer tip), the row insert, and the conversation’s activity/tip update. Appending never prunes — only create does, matching the JSON backend.

Chain (§6): events and token counts are row content — the v1 canonical encoding has length-prefixed the serialized events string and the token presence bytes since 17.1a, so populated values hash under the exact rules empty ones did. No encoding_version bump: pre-17.6 rows ('[]', NULL) and 17.6 rows verify under the same v1 dispatch, and tampering with a stored event breaks verify_chain like any other field.

Tokens are measurements, not estimates: pass the backend’s reported counts or None. None is stored as NULL — absence stays observable (18.5 rehydrates from these columns and must be able to trust them; gates-are-honest).

FTS: the 17.3 AFTER INSERT trigger derives tool_names / tool_args_digest from the events JSON at index time — recording events here lights recall up with no schema work.

Phantom reaches (#717): the per-turn alias-seam telemetry persists alongside events in its own phantom_reaches column. It is deliberately NOT part of the §6 canonical encoding (telemetry, not provenance), so an older db gains the column on open and existing content chains verify byte-for-byte unchanged. Folding it into the hash would require a v2 encoding bump — a deliberate follow-up, not this additive change.

Source

pub fn load(&self, id: &str) -> Result<ConversationRecord>

Load a full record (turns in causal (writer, seq) order). id may be a unique prefix.

Source

pub fn load_turn(&self, id: &str, seq: i64) -> Result<Option<ConversationTurn>>

Read ONE past turn by its (conversation, seq) address — the by-id read the memory_fetch tool’s turn:<conv>#<seq> resolver needs (progressive-disclosure memory, Workstream A MVP, #319). id may be a unique prefix (same resolve_id discipline as Self::load); seq is the §6 per-writer tick the model was shown by a recall hit (SearchHit::seq).

Workspace-fenced: the conversations join carries workspace_key, so a seq from another workspace’s conversation resolves to None, never a cross-workspace leak (§7 fencing). Returns Ok(None) when no turn at that (conversation, seq) exists — labelled absence, never an error — so the tool executor can answer “no such memory item” rather than aborting the loop.

Source

pub fn list(&self) -> Result<Vec<ConversationSummary>>

All conversations in this workspace, least-recently-active first — “active” meaning the §6 activity tick, never a timestamp. The summaries’ updated_at_unix_nanos is the display claim.

Source

pub fn latest_open(&self) -> Result<Option<ConversationSummary>>

The most-recently-active open conversation in this workspace — highest activity_tick whose end_reason is still NULL — or None when every conversation has been ended (or none exist). This is the auto-resume target: an ended conversation (/end, /restart, :wq) is skipped here so the next launch does not silently re-enter it, yet it stays in list / /recall because it is not deleted.

Source

pub fn end_conversation(&self, id: &str, reason: &str) -> Result<()>

Mark a conversation ended with a short reason ("new", "restart", "wq", …). Like rename this is metadata, not activity: it does NOT tick the §6 clock, so it cannot perturb MRU ordering — it only sets end_reason (the column reserved at 17.7), which latest_open reads to skip the row on auto-resume. The conversation, its turns, and its FTS rows are untouched, so /recall and /conversation still find it. Idempotent and workspace-fenced (an id from another workspace resolves as absent).

#1030: bind (or clear) a conversation’s place in a roadmap tree — the Plan node whose context window this conversation IS. roadmap_id + node_id together locate the crate::plan::Subtask node; passing None/None clears the link (back to an ad-hoc chat). Workspace-fenced and idempotent. Like rename this is metadata, not activity: it does NOT tick the §6 clock, so it cannot perturb MRU ordering — the pointers ride the row exactly like scratchpad/plan.

Source

pub fn claim(&self, id: &str) -> Result<ClaimOutcome>

#1030 collision fix: attempt to become the SINGLE live owner of id. Atomic (BEGIN IMMEDIATE): if the conversation is unclaimed, or its claim is our own, or its claim is STALE (the owner is not live — a crashed or rebooted process), this process takes ownership and returns Claimed. If a DIFFERENT, LIVE process owns it, returns HeldBy and writes nothing — the caller must not attach (attaching is exactly the turn-interleaving bug #1030 fixes). Identity is host+boot_id+pid, never the (machine-shared) writer fingerprint.

Source

pub fn release(&self, id: &str) -> Result<()>

Release THIS process’s claim on id (best-effort). Only deletes a claim this exact process holds (host+boot_id+pid), so it can never free another live session’s conversation. Called on clean exit / conversation switch; a crash simply leaves a stale claim the next claim reclaims. A missing / foreign id is a silent no-op.

Source

pub fn heartbeat(&self, id: &str) -> Result<()>

Refresh THIS process’s heartbeat on id — the freshness signal a cross-host / post-reboot liveness check reads. Cheap; meant to piggyback the per-turn save. No-op if this process does not hold the claim.

Source

pub fn live_owner(&self, id: &str) -> Result<Option<StoredOwner>>

The raw live_owners row for id, WITHOUT a liveness judgement — None when unclaimed. /resume pairs this with is_owner_live to render each conversation’s ● live / ○ open marker.

Source

pub fn is_owner_live(&self, owner: &StoredOwner) -> bool

Whether owner is a running process right now, per the store’s (injected) liveness oracle — the SAME judgement claim uses, exposed so /resume renders a consistent marker.

Source

pub fn create_roadmap(&self, id: &str, title: &str, tree: &Plan) -> Result<()>

Create (or overwrite) a roadmap with id, title, and tree — the serialized crate::plan::Plan of Roadmap/Phase/Plan/Task nodes. Workspace-fenced on write as well as read (#1086): the roadmaps primary key is (id, workspace_key), so INSERT OR REPLACE can only ever replace this workspace’s same-id row — importing an id that exists under another workspace inserts a separate row, never steals it. Overwrite-within-a-workspace is intentional (re-create replaces the tree), matching the conversation store’s create_with_id semantics.

Source

pub fn load_roadmap(&self, id: &str) -> Result<Option<Roadmap>>

Load roadmap id (workspace-fenced), deserializing its tree blob back into a crate::plan::Plan. None when absent. A tree column that is not valid plan TOML is a hard error — never hand back a garbage tree.

Source

pub fn update_roadmap(&self, id: &str, tree: &Plan) -> Result<()>

Replace roadmap id’s tree (workspace-fenced). A no-op if absent.

Source

pub fn list_roadmaps(&self) -> Result<Vec<RoadmapSummary>>

This workspace’s roadmaps, most-recently-updated first.

Source

pub fn rename(&self, id: &str, title: &str) -> Result<()>

Rename a conversation. Updates the display claim but does NOT tick the activity clock: a rename is metadata, not activity, so it cannot perturb MRU ordering (§6 dissolved the old rename-bumps-updated_at defect, design doc §1).

Source

pub fn update_scratchpad( &self, id: &str, scratchpad: &BTreeMap<String, String>, ) -> Result<()>

Persist a conversation’s scratchpad <state> snapshot (#713). The map is serialized to JSON and written to the conversation row’s scratchpad column so an interrupt + auto-resume can re-hydrate the live store.

Like rename / end_conversation this is metadata, not activity: it does not tick the §6 clock, so it cannot perturb MRU ordering, and the scratchpad is NOT part of the §6 content chain (it rides the conversation row, never a turn’s canonical encoding) — working memory, not provenance. Workspace-fenced and idempotent: an id from another workspace resolves as absent and the UPDATE matches nothing.

Source

pub fn update_plan_snapshot(&self, id: &str, plan: &PlanSnapshot) -> Result<()>

Persist a conversation’s plan-ledger snapshot (#715). The crate::PlanSnapshot is serialized to JSON and written to the conversation row’s plan column so an interrupt + auto-resume can re-hydrate the live ledger (the <plan> block + plan_get survive).

Like update_scratchpad this is metadata, not activity: it does not tick the §6 clock, so it cannot perturb MRU ordering, and the plan is NOT part of the §6 content chain (it rides the conversation row, never a turn’s canonical encoding) — working memory, not provenance. Workspace-fenced and idempotent: an id from another workspace resolves as absent and the UPDATE matches nothing.

Source

pub fn delete(&self, id: &str) -> Result<()>

Delete a conversation (its turns cascade) and, best-effort, its per-session plan dir (issue #220).

Source

pub fn resolve_id(&self, id_or_prefix: &str) -> Result<String>

Resolve an exact id or unique prefix within this workspace.

Source

pub fn verify_chain(&self, id: &str) -> Result<()>

Verify the §6 content chain for a conversation: every writer’s turns must link prev_hash → BLAKE3(prior turn’s canonical encoding) from the genesis hash, and the stored chain tip must match this writer’s last turn. A tampered row (content OR claims — claims are inside the canonical encoding, so they are tamper-evident too) breaks the chain.

Source

pub fn search(&self, query: &str, limit: usize) -> Result<Vec<SearchHit>>

Full-text recall over this workspace’s turns (17.3, issue #246).

The raw query goes through sanitize_fts5_query (an empty result after sanitizing is an error, never a match-all), then a MATCH against the trigger-maintained turns_fts index, ranked by bm25 (best first), fenced to this workspace by joining conversations.workspace_key. Each hit carries a snippet() of the matched column — the match wrapped in >>>/<<<, roughly ±10 tokens of context, at trimmed edges. Snippets are the whole payload by design: no full turn content, no aux-LLM recaps (the design doc explicitly skips those — slow and expensive on local models; the hermes study’s own “snippet is enough, saves tokens”).

Trait Implementations§

Source§

impl Clone for ConversationStore

Source§

fn clone(&self) -> ConversationStore

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for ConversationStore

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> ErasedDestructor for T
where T: 'static,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts 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
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more