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
impl ConversationStore
Sourcepub fn new(
root: impl AsRef<Path>,
workspace: impl AsRef<Path>,
max_per_workspace: usize,
) -> Result<Self>
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.
Sourcepub 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.
pub fn workspace_id_for_path(path: impl AsRef<Path>) -> Result<String>
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.
Sourcepub fn wal_fallback_notice(&self) -> Option<&str>
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.
Sourcepub fn writer_fingerprint(&self) -> &str
pub fn writer_fingerprint(&self) -> &str
This install’s writer fingerprint — the writer_fingerprint half of
the §6 (writer_fingerprint, seq) ordering key.
Sourcepub fn create(&self, title: &str, persona: Option<&str>) -> Result<String>
pub fn create(&self, title: &str, persona: Option<&str>) -> Result<String>
Create a conversation with a freshly minted id; returns the id.
Sourcepub fn create_with_id(
&self,
id: &str,
title: &str,
persona: Option<&str>,
) -> Result<()>
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.
Sourcepub fn exists(&self, id: &str) -> Result<bool>
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.
Sourcepub fn append_turn(&self, id: &str, user: &str, assistant: &str) -> Result<()>
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.
Sourcepub 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<()>
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.
Sourcepub fn load(&self, id: &str) -> Result<ConversationRecord>
pub fn load(&self, id: &str) -> Result<ConversationRecord>
Load a full record (turns in causal (writer, seq) order). id may
be a unique prefix.
Sourcepub fn load_turn(&self, id: &str, seq: i64) -> Result<Option<ConversationTurn>>
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.
Sourcepub fn list(&self) -> Result<Vec<ConversationSummary>>
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.
Sourcepub fn latest_open(&self) -> Result<Option<ConversationSummary>>
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.
Sourcepub fn end_conversation(&self, id: &str, reason: &str) -> Result<()>
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).
Sourcepub fn link_conversation_to_node(
&self,
id: &str,
roadmap_id: Option<&str>,
node_id: Option<&str>,
) -> Result<()>
pub fn link_conversation_to_node( &self, id: &str, roadmap_id: Option<&str>, node_id: Option<&str>, ) -> Result<()>
#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.
Sourcepub fn claim(&self, id: &str) -> Result<ClaimOutcome>
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.
Sourcepub fn release(&self, id: &str) -> Result<()>
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.
Sourcepub fn heartbeat(&self, id: &str) -> Result<()>
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.
Sourcepub fn live_owner(&self, id: &str) -> Result<Option<StoredOwner>>
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.
Sourcepub fn is_owner_live(&self, owner: &StoredOwner) -> bool
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.
Sourcepub fn create_roadmap(&self, id: &str, title: &str, tree: &Plan) -> Result<()>
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.
Sourcepub fn load_roadmap(&self, id: &str) -> Result<Option<Roadmap>>
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.
Sourcepub fn update_roadmap(&self, id: &str, tree: &Plan) -> Result<()>
pub fn update_roadmap(&self, id: &str, tree: &Plan) -> Result<()>
Replace roadmap id’s tree (workspace-fenced). A no-op if absent.
Sourcepub fn list_roadmaps(&self) -> Result<Vec<RoadmapSummary>>
pub fn list_roadmaps(&self) -> Result<Vec<RoadmapSummary>>
This workspace’s roadmaps, most-recently-updated first.
Sourcepub fn rename(&self, id: &str, title: &str) -> Result<()>
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).
Sourcepub fn update_scratchpad(
&self,
id: &str,
scratchpad: &BTreeMap<String, String>,
) -> Result<()>
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.
Sourcepub fn update_plan_snapshot(&self, id: &str, plan: &PlanSnapshot) -> Result<()>
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.
Sourcepub fn delete(&self, id: &str) -> Result<()>
pub fn delete(&self, id: &str) -> Result<()>
Delete a conversation (its turns cascade) and, best-effort, its per-session plan dir (issue #220).
Sourcepub fn resolve_id(&self, id_or_prefix: &str) -> Result<String>
pub fn resolve_id(&self, id_or_prefix: &str) -> Result<String>
Resolve an exact id or unique prefix within this workspace.
Sourcepub fn verify_chain(&self, id: &str) -> Result<()>
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.
Sourcepub fn search(&self, query: &str, limit: usize) -> Result<Vec<SearchHit>>
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
impl Clone for ConversationStore
Source§fn clone(&self) -> ConversationStore
fn clone(&self) -> ConversationStore
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreAuto Trait Implementations§
impl Freeze for ConversationStore
impl RefUnwindSafe for ConversationStore
impl Send for ConversationStore
impl Sync for ConversationStore
impl Unpin for ConversationStore
impl UnsafeUnpin for ConversationStore
impl UnwindSafe for ConversationStore
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
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> ErasedDestructor for Twhere
T: 'static,
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