Repository navigation
Description
Status: Draft / scoping. Filed as a follow-up to #1196 to capture the structural shape of the fix; not scheduled.
Summary
Stop maintaining threadSlice.threads and threadSlice.messagesByThreadId as a parallel client-side cache. Replace consumers with direct RPC reads against openhuman.threads_list / openhuman.thread_messages (the Rust core already persists these as JSONL upsert logs in src/openhuman/memory/conversations/store.rs), wrapped in a stale-while-revalidate layer (react-query or equivalent). Keep selectedThreadId as a small persisted UI atom.
Problem / Context
The Rust core is already the source of truth — threads live in a JSONL upsert log under the workspace dir, and threads_list is exposed over JSON-RPC. The React app shadows that as a Redux slice (threads, messagesByThreadId, messages) for sync render-time access and optimistic updates. Maintaining two stores creates a steady stream of cache-coherence bugs:
- fix(threads): persist selectedThreadId across reloads #1168 / fix(threads): preserve selectedThreadId on cold-boot identity hydration #1196 — the persisted
selectedThreadIdwas wiped byclearAllThreads()on the boot-time identity-observation path because the slice conflates selection with cache. - Fix new signup showing existing/stale chat threads #1157 — pre-auth thread rows had to be explicitly scrubbed from the slice when identity hydrated.
- Multiple ad-hoc invalidation paths in
CoreStateProviderandConversations.tsxexist to keep the cache in sync.
Going cache-less makes this entire class of bug structurally impossible. The cost is a non-trivial refactor — see Scope.
Scope
In scope
- Introduce a query layer over
coreRpcClientfor thread reads (useThreads(),useThreadMessages(id)). Stale-while-revalidate semantics, error/loading states surfaced to consumers, automatic refetch on identity change. - Migrate ~30
useAppSelector(s => s.thread.*)call sites inapp/src/pages/Conversations.tsx,app/src/components/**,app/src/providers/ChatRuntimeProvider.tsx, etc. - Migrate the streaming append path in
ChatRuntimeProvider— today it dispatchesaddInferenceResponse/addMessageLocalthunks that mutatemessagesByThreadId. Needs to push into the query cache instead (or hold the in-flight turn in component-local state and let the post-turn refetch reconcile). - Migrate optimistic UI for rename / delete / new-thread to query-cache mutations.
- Reduce
threadSliceto just{ selectedThreadId, activeThreadId }. Dropthreads,messagesByThreadId,messages,welcomeThreadId(already a no-op per feat(onboarding): replace welcome-agent bot with react-joyride walkthrough #1180), and theloadThreads/loadThreadMessagesthunks. - Remove
resetThreadCachesPreservingSelection(added in fix(threads): preserve selectedThreadId on cold-boot identity hydration #1196 as a workaround) and the boot-time scrub branch inCoreStateProvider.tsx:259— no in-memory cache means nothing to scrub.
Out of scope
- Any change to the Rust storage layer or RPC surface. The JSONL upsert log is fine as-is.
- Realtime push from core → app for thread list changes (currently a poll/refetch model). Could be revisited separately if latency becomes a UX concern.
- Replacing Redux as a whole — non-thread slices stay.
Tradeoffs
- Adds a query-layer dependency (likely react-query) — already a common pattern, but a new top-level concept in this app.
- Streaming UX is the trickiest piece: tokens arrive ~10–50/s and currently mutate the slice cheaply. Need to either (a) buffer in component state during the turn and refetch on
chat_done, or (b) write directly into the query cache with manual invalidation. (a) is simpler but flickers if the refetch is slow; (b) preserves current behavior.
Acceptance criteria
- Query hooks land —
useThreads()anduseThreadMessages(id)exist, hitcoreRpcClient, and have unit tests covering loading/error/refetch. - All consumers migrated — no remaining
useAppSelectorreferences tostate.thread.threadsorstate.thread.messagesByThreadId. Streaming append flows through the new layer. -
threadSliceshrinks — only{ selectedThreadId, activeThreadId }remain. The boot-time scrub branch inCoreStateProvideris removed. - Regression coverage — existing fix(threads): persist selectedThreadId across reloads #1168 / Fix new signup showing existing/stale chat threads #1157 / fix(auth): stop stale chat threads after signup #1192 behaviors still hold (reload resumes the user's thread; pre-auth rows don't leak across identity changes; signup doesn't show stale titles).
- Diff coverage ≥ 80% — the implementing PR meets the changed-lines coverage gate (Vitest + cargo-llvm-cov, enforced by
.github/workflows/coverage.yml) when code changes are involved.
Related
- fix(threads): preserve selectedThreadId on cold-boot identity hydration #1196 — fix(threads): preserve
selectedThreadIdon cold-boot identity hydration (the patch this issue would obsolete) - fix(threads): persist selectedThreadId across reloads #1168 — original
selectedThreadIdpersistence work - Fix new signup showing existing/stale chat threads #1157 / fix(auth): stop stale chat threads after signup #1192 — pre-auth stale-thread scrub that motivated
clearAllThreads() src/openhuman/memory/conversations/store.rs— JSONL upsert log (source of truth)app/src/store/threadSlice.ts— slice to be shrunkapp/src/providers/ChatRuntimeProvider.tsx— streaming append path
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity