Conversation thread and message management. Owns the RPC surface and controller registry for thread lifecycle (create / list / upsert / delete / purge), per-thread message CRUD, AI-assisted thread titling, persisted in-flight turn snapshots (for cold-boot recovery of interrupted agent turns), the per-thread kanban task board passthrough, and a one-shot welcome-agent → orchestrator workspace migration. Persistence is delegated to memory::conversations (JSONL thread/message store) and to a dedicated turn_state snapshot store; this module is the RPC/controller layer over both.
- List, create (
upsertwith caller-supplied id orcreate_newwith auto-generated id + placeholder title), delete, and purge conversation threads. - List, append, and metadata-patch messages within a thread.
- Update thread labels and user-specified titles.
- Generate a durable thread title from the first user message + assistant reply via the inference provider, with deterministic fallbacks (derive title from the user message; skip non-placeholder titles).
- Maintain restart-survivable snapshots of in-flight agent turns (
turn_state): get / list / clear over RPC; written by the web-channel progress bridge viaTurnStateMirror. - Expose a per-thread kanban task board get/put that proxies to
agent::task_board. - On thread delete/purge, invalidate the in-process web-channel session and clean up orphaned turn snapshots.
- One-shot idempotent migration of legacy welcome-agent artifacts (strip
onboardinglabel, renamewelcome*session transcripts/markdown toorchestrator*).
| File | Role |
|---|---|
src/openhuman/threads/mod.rs |
Export-focused: module docstring, mod decls, re-exports (ThreadsError, THREAD_NOT_FOUND_KIND, controller schema/registry pair, welcome-migration entry point). |
src/openhuman/threads/ops.rs |
Business logic / RPC entry points returning RpcOutcome<ApiEnvelope<T>>. All thread + message + turn-state ops live here; wraps results in ApiEnvelope with request-id/count meta. |
src/openhuman/threads/schemas.rs |
ControllerSchema definitions, all_controller_schemas, all_registered_controllers, and handle_* functions delegating to ops. Also hosts the task_board_get/task_board_put handlers (proxy to agent::task_board). |
src/openhuman/threads/error.rs |
ThreadsError taxonomy (NotFound / Message); encodes NotFound as a StructuredRpcError (kind: "ThreadNotFound", expected_user_state: true) at the controller boundary so the frontend handles stale thread refs without string-matching or Sentry noise. |
src/openhuman/threads/title.rs |
Pure, provider-free helpers: placeholder-title detection, raw-title sanitization, whitespace collapse, fallback title from user message, prompt builder, log fingerprint. Heavily unit-tested. |
src/openhuman/threads/welcome_migration.rs |
One-shot, marker-guarded migration of legacy welcome-agent threads/transcripts to orchestrator naming. |
src/openhuman/threads/turn_state/mod.rs |
Submodule export hub for in-flight turn snapshots. |
src/openhuman/threads/turn_state/types.rs |
Wire/storage types: TurnState, TurnLifecycle, TurnPhase, ToolTimelineEntry/Status, SubagentActivity/ToolCall, and the get/list/clear request/response payloads. camelCase to mirror chatRuntimeSlice.ts. |
src/openhuman/threads/turn_state/store.rs |
TurnStateStore — atomic per-thread JSON snapshot store (tempfile + persist + dir fsync), process-wide mutex; put/get/delete/list/clear_all/mark_all_interrupted plus free-fn wrappers. |
src/openhuman/threads/turn_state/mirror.rs |
TurnStateMirror — translates agent::progress::AgentProgress events into TurnState mutations (incl. the ordered narration/thinking/tool transcript), flushing at iteration/tool boundaries; on completion marks the snapshot Completed and keeps it (so the "View processing" panel can replay the finished turn), flags Interrupted if the bridge exits without TurnCompleted. |
*_tests.rs |
Sibling test suites: ops_tests.rs, schemas_tests.rs, turn_state/store_tests.rs, turn_state/mirror_tests.rs (plus inline tests in error.rs, title.rs, welcome_migration.rs). |
From mod.rs:
ThreadsError,THREAD_NOT_FOUND_KIND(re-exported fromerror).all_threads_controller_schemas/all_threads_registered_controllers(re-exported fromschemas).migrate_welcome_agent_artifacts,WelcomeMigrationResult(re-exported fromwelcome_migration).
From turn_state: TurnStateMirror, TurnStateStore, TurnState + lifecycle/phase/timeline/subagent types and the turn-state RPC request/response types.
Namespace threads (JSON-RPC openhuman.threads_<function>). Schemas + handlers registered via all_registered_controllers:
| Function | Op |
|---|---|
list |
List thread summaries. |
upsert |
Create/refresh a thread (caller id, title, created_at, optional labels). |
create_new |
New thread with auto id + Chat <date> <time> placeholder title. |
messages_list |
List messages for a thread. |
message_append |
Append a message (returns typed ThreadsError, i.e. structured NotFound). |
message_update |
Patch extra_metadata on a message. |
generate_title |
LLM-generate title from first user + assistant message, with fallbacks. |
update_labels |
Replace labels (empty vec clears all labels). |
update_title |
Set a user-specified title (rejects empty). |
delete |
Delete thread + message log; invalidates web session, clears turn snapshot. |
purge |
Remove all threads/messages; clear_all turn snapshots. |
turn_state_get / turn_state_list / turn_state_clear |
Read / list / delete persisted in-flight turn snapshots. |
task_board_get / task_board_put |
Proxy the per-thread kanban board to agent::task_board. |
Wired into the registry from src/core/all.rs (controllers + schemas extended with the all_threads_* pair).
- Threads + messages: delegated to
memory::conversations(JSONL store under the workspace), not owned here. Every call goes throughmemory_conversations::blocking::*(tokio::task::spawn_blocking) — never the sync entry points directly, see the note below. - Turn snapshots (
turn_state/store.rs): one JSON file per thread at<workspace>/memory/conversations/turn_states/<hex(thread_id)>.json. Whole-file atomic overwrite (tempfile → fsync → persist → best-effort dir fsync), serialized through a process-wideparking_lot::Mutex. A non-terminal file surviving cold boot is markedInterrupted; aCompletedsnapshot is intentionally retained (for processing replay) and skipped by startup interrupted-marking. The next turn on the thread overwrites it. - Task board: persisted by
agent::task_board::TaskBoardStoreunder the workspace (this module only proxies). - Migration marker:
state/migrations/welcome_to_orchestrator_v1.doneguards the welcome migration.
crate::openhuman::memory/memory_conversations— thread + message store types and CRUD (ensure_thread,list_threads,get_messages,append_message,update_thread_*,ConversationStore, etc.); also theApiEnvelope/ApiMeta/request/response DTOs.crate::openhuman::config::Config— resolvesworkspace_dirand inference/runtime/secrets settings (load_or_init).crate::openhuman::inference::provider— builds the intelligent-routing provider used for AI title generation (create_intelligent_routing_provider,ProviderRuntimeOptions).crate::openhuman::web_chat—invalidate_thread_sessionson thread delete (so a deleted thread's live web session can't keep appending).crate::openhuman::agent::task_board—TaskBoard,TaskBoardCard,TaskBoardStore,board_for_threadfor the task-board RPCs; also the optionaltask_boardfield onTurnState.crate::openhuman::agent::progress::AgentProgress— progress events consumed byTurnStateMirror.crate::core::all—ControllerFuture,RegisteredControllerfor the registry.crate::core::{ControllerSchema, FieldSchema, TypeSchema}— schema definitions.crate::rpc::{RpcOutcome, StructuredRpcError}— RPC outcome wrapper and structured-error encoding.
src/core/all.rs— registers the controllers/schemas into the JSON-RPC + CLI registry.src/core/jsonrpc.rs— references threads (transport routing).src/openhuman/web_chat/— drivesTurnStateMirror/ turn-state store during chat turns; consumesinvalidate_thread_sessions.src/openhuman/platform/startup/ops.rs— invokes the welcome migration and/or turn-state startup handling.
- Never call the sync
memory_conversationsAPI from these handlers — usememory_conversations::blocking::*. Each store entry point takes a process-globalparking_lot::Mutexand then does fsync'd JSONL IO while holding it, and the per-call cost grows with the user's history (threads.jsonlis folded on nearly every operation and gains ~2 lines per message, uncompacted). Called inline, a handler parks a tokio worker thread on that mutex; once more conversation ops are queued than there are workers, the runtime stops polling anything — including the HTTP task that owes the client a response — and a one-append create blows the frontend's 30 s RPC budget (UnhandledRejection: Core RPC openhuman.threads_create_new timed out after 30000ms, Sentry TAURI-REACT-10 / #5156). The wrappers keep the lock wait on the blocking pool; the store is just as serialized, but the executor stays live.welcome_migration.rsis the deliberate exception: it is a sync, one-shot, marker-guarded boot migration, not a request path. generate_titleonly replaces titles matching theChat <Mon> <d> <h>:<mm> <AM|PM>placeholder shape (is_auto_generated_thread_title); user-renamed threads are never overwritten. Provider/init/sanitization failures degrade to a deterministic fallback title derived from the first user message — never an error.deleteinvalidates the web-channel session before turn-snapshot cleanup (ordering is load-bearing per the inline comment); snapshot-cleanup failure surfaces as an RPC error so callers see a partial failure rather than silent on-disk drift.purgeusesclear_all(not list+delete) so corrupted/half-written snapshot files — whichlist()warn-skips — are also removed.ThreadsError::NotFoundis the only placeThreadNotFoundbecomes a wire-shaped structured error; the transport layer does not sniff method names or error strings.from_thread_scoped_store_erroronly promotes toNotFoundwhen the parsed id matches the requested thread id, to avoid clearing the wrong stale thread on the frontend.- Turn-state types intentionally serialize camelCase to mirror
app/src/store/chatRuntimeSlice.tsso a snapshot applies to the slice without translation. TurnStateMirrorflushes only at iteration/tool boundaries; high-frequency deltas (streaming text, thinking, tool args) mutate memory only, to avoid filesystem thrash under streaming load.- Directory fsync after snapshot rename is best-effort and a no-op on Windows (relies on NTFS journaling).
- The welcome migration is idempotent (marker-guarded) and fails closed: a destination-collision or any per-item failure returns a
partial migrationerror and does not write the marker, so a later retry can resume.