Skip to content

feat(conversations): per-turn process history (timeline refactor Phases 1–2, 4–5)#4612

Merged
senamakel merged 14 commits into
tinyhumansai:mainfrom
senamakel:feat/conversations-per-turn-history
Jul 7, 2026
Merged

feat(conversations): per-turn process history (timeline refactor Phases 1–2, 4–5)#4612
senamakel merged 14 commits into
tinyhumansai:mainfrom
senamakel:feat/conversations-per-turn-history

Conversation

@senamakel

@senamakel senamakel commented Jul 6, 2026

Copy link
Copy Markdown
Member

Summary

  • Per-turn process history end-to-end: every answer in a multi-turn thread now keeps its own "Agentic task insights" trail instead of losing it on the next send — the fix for tab-switch / reload context loss.
  • Rust per-turn ring store: turn snapshots move from one-file-per-thread (latest wins) to a bounded per-turn ring (turn_states/<hex(thread)>/<hex(request)>.json, retention N=20), with idempotent legacy migration and unchanged public signatures (zero caller churn).
  • New RPCs threads.turn_state_history / turn_state_get_turn expose the per-turn history without disturbing the cold-boot turn_state_list.
  • Frontend timeline architecture: the conversations panel moved to app/src/features/conversations/; a memoized TimelineItem projection (selectTimelineForThread) is now the single source of render order, and assistant messages are stamped with extraMetadata.requestId so turns group deterministically.
  • Delivered as 10 small, individually-reviewable commits (Phases 1, 2, 4, 5 of the plan), tests green throughout.

Problem

app/src/pages/Conversations.tsx (3,302 lines) held one tool-timeline array per thread and the Rust core persisted one turn snapshot per thread (whole-file overwrite). So a multi-turn thread lost every turn's process trail but the latest, and each send wiped the live timeline. See docs/plans/conversations-timeline-refactor.md and docs/plans/per-turn-tool-timeline-history.md (both landed on this branch).

Solution

Frontend-first, then the backend slots in underneath:

  1. Phase 1 — relocate the panel + its subtree to features/conversations/ behind a thin pages/Conversations.tsx shim; rename the AgentChatPanel page-alias to ConversationsPage (resolves the collision with the settings panel).
  2. Phase 2 — a TimelineItem discriminated union + memoized projection that reproduces today's anchoring exactly (single tool timeline after the last user message; proactive/no-user-message fallback; hideAgentInsights filtering). The live render loop is then sourced from the projection DOM-identically.
  3. Phase 4 — stamp extraMetadata.requestId on every assistant append (chat_done incl. parallel/segment-reconcile paths, chat_segment, chat_interim, proactive); rebuild the store as a per-turn ring with retention + legacy migration; add the history RPCs + threadApi clients.
  4. Phase 5 — hydrate the per-turn history (turn_state_history) on thread open into an additive turnTimelinesByThread slice field (which survives a new send), and render each older turn's collapsed ToolTimelineBlock above the answer it produced. The newest turn is excluded (it still renders as the live anchor), so nothing double-renders.

Deliberately deferred (own follow-up PR): Phase 3 (ChatRuntimeProvider → reducer consolidation + dedup unification + preserveLiveSubagentProse removal) — a large refactor of the live streaming path whose dedup changes the plan flags as risk-sensitive and which needs manual live-streaming QA; Phase 6 cleanup (shim deletion / dead-state removal, mostly coupled to Phase 3); and the Phase 1 shell extraction.

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) — selector projection (20), renderer (6), provider requestId stamping, Rust store (5 new: retention/migration/get_turn/list_thread/live-not-pruned), extended JSON-RPC E2E (history/get_turn/absent), frontend per-turn hydration (skip-latest + lifecycle filter + transport-error) and render.
  • Diff coverage ≥ 80% — not measured locally in this session; the changed lines are heavily tested (see above) but please confirm via the CI coverage gate.
  • Coverage matrix updated — N/A: no feature IDs added/removed/renamed in docs/TEST-COVERAGE-MATRIX.md; behaviour is additive within the existing conversations/threads features.
  • All affected feature IDs from the matrix are listed under ## RelatedN/A (see above).
  • No new external network dependencies introduced.
  • Manual smoke checklist updated — N/A: no new release-cut surface; recommend a manual eyeball of a multi-turn thread (per the plan's Phase 5 QA note) before merge.
  • Linked issue closed — N/A: no tracking issue.

Impact

  • Desktop/web (conversations panel). No mobile/CLI surface change.
  • Migration: old flat <hex(thread)>.json snapshots are read once and migrated to the per-turn layout on first access (idempotent); the read path is kept one release. get/list/delete/clear_all/mark_all_interrupted semantics are preserved, so all existing callers and the cold-boot path are unaffected.
  • Performance: get/list scan a small per-thread directory (≤ retention window); completed turns prune to N=20 per thread so history stays bounded. Selector is reselect-memoized per thread.
  • Compatibility: purely additive slice field + RPCs; threads with legacy (un-stamped) messages fall back to today's single-anchor behavior.

Related

  • Closes:
  • Follow-up PR(s)/TODOs: Phase 3 (provider/reducer consolidation), Phase 6 (cleanup), Phase 1 remainder (shell extraction).

AI Authored PR Metadata

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: feat/conversations-per-turn-history
  • Commit SHA: see PR head

Validation Run

  • pnpm --filter openhuman-app format:check
  • pnpm typecheck
  • Focused tests: full frontend suite (8,327 passed / 0 failed); Rust turn_state::store (14), turn_state::mirror (12), threads::ops (34), threads::schemas (17), json_rpc_thread_turn_state_lifecycle E2E; chat Playwright web specs (send-stream, scroll-render, multi-turn history).
  • Rust fmt/check (if changed): cargo fmt --check clean; pnpm rust:check (Tauri crate) passed via pre-push hook.
  • Tauri fmt/check (if changed): N/A — no app/src-tauri changes.

Validation Blocked

  • command: N/A
  • error: N/A
  • impact: N/A

Behavior Changes

  • Intended behavior change: past turns in a multi-turn thread now render their own tool-timeline trail (previously lost on the next send / reload).
  • User-visible effect: scrolling up a multi-turn thread shows each answer's process steps above it.

Parity Contract

  • Legacy behavior preserved: messages without requestId fall back to the single-anchor timeline; the render-loop swap is DOM-identical (full Conversations render suite green); store get/list/delete/clear_all/mark_all_interrupted signatures + semantics unchanged.
  • Guard/fallback/dispatch parity checks: schema parity test updated for the two new RPCs; render test asserts no past-turn block before hydration and exactly one after.

Duplicate / Superseded PR Handling

  • Duplicate PR(s): none
  • Canonical PR: this
  • Resolution: N/A

Summary by CodeRabbit

  • New Features

    • Conversations now show a richer, unified timeline with user messages, assistant replies, streaming previews, and grouped process activity.
    • Added past-turn history support so older conversation turns can be viewed and restored alongside the current thread.
    • Improved chat and notification rendering with updated conversation component sharing.
  • Bug Fixes

    • Conversation items now load and display more consistently across screens.
    • Historical turn data is now preserved and retrievable by specific request.

senamakel added 11 commits July 6, 2026 14:16
Audit of Conversations.tsx (rendering of tool calls/thoughts/subagents,
ordering, storage/streaming, tab-switch context loss) and a 6-phase
refactor plan: unified TimelineItem projection, component decomposition
into features/conversations/, reducer-side merge/dedup consolidation,
and the per-turn turn-state ring store (adopts
per-turn-tool-timeline-history.md as the backend track).

Claude-Session: https://claude.ai/code/session_01NsLNxq4vPbhverA3aCDtzs
…Phase 1a)

Mechanical move of `app/src/pages/conversations/{components,hooks,utils,
composerSendDecision,taskPlanActions}` to `app/src/features/conversations/`,
matching the `features/human/` layout per the timeline-refactor plan. Pure
relocation — no behavior change. `pages/` and `features/` sit at the same depth
under `app/src`, so the moved files' own imports are unchanged; only references
*to* the subtree were repointed (Conversations.tsx, pages/dev preview, and 9
external importers under components/{chat,intelligence,flows,notifications,
channels,layout}).

Typecheck + lint clean; 337 tests across the moved subtree and all consumers
(Conversations render suite, HumanPage, Accounts, IntelligenceTasksTab) pass.

Refs: docs/plans/conversations-timeline-refactor.md Phase 1

Claude-Session: https://claude.ai/code/session_01MVwyyQVNm8UZpvJyWrf2yo
…se 1b)

Relocate the monolithic `Conversations` component to
`app/src/features/conversations/Conversations.tsx`; `pages/Conversations.tsx`
becomes a thin re-export shim so `HumanPage` and the co-located test suites keep
their current import paths during migration (removed in Phase 6). Add
`features/conversations/index.ts` as the public reusability contract.

Resolve the `AgentChatPanel` name collision with the unrelated
`components/settings/panels/AgentChatPanel.tsx`: the page-variant alias is
renamed `ConversationsPage`; `Accounts.tsx` and its webview-selection test mock
updated to match. All relative imports in the moved file deepened one level
(`../` → `../../`); no behavior change.

Typecheck + lint clean; 118 tests (Conversations render/attachments/overflow/
welcomeLock/helpers, HumanPage, Accounts) pass.

Refs: docs/plans/conversations-timeline-refactor.md Phase 1

Claude-Session: https://claude.ai/code/session_01MVwyyQVNm8UZpvJyWrf2yo
Additive data model + memoized selector that project threadSlice messages and
chatRuntimeSlice's tool timeline / streaming previews into one ordered
`TimelineItem[]`. No renderer change yet — nothing consumes it, so zero
regression risk; this locks the ordering/anchoring contract with tests ahead of
the render-loop swap.

- `timeline/types.ts`: the discriminated `TimelineItem` union (userMessage,
  assistantMessage, streamingText, reasoning, toolCall, subagentActivity),
  `TimelineTurn` grouping, `LEGACY_TURN_ID`, and status/subagent helpers.
- `timeline/selectors.ts`: pure `buildThreadTimeline` + memoized
  `selectTimelineForThread`. Reproduces today's behavior exactly — messages in
  append order (hidden dropped), the single tool timeline anchored after the
  last user message, the proactive/no-user-message fallback trailing the
  messages, streaming previews (primary + forked branches) trailing durable
  items, and `hideAgentInsights` omitting the process kinds. Until Phase 4
  stamps requestId, everything is one `legacy` turn.

17 unit tests (ordering, anchor splice, success→ok mapping, subagent
classification, empty/proactive/hidden fallbacks, hideAgentInsights, streaming
trailing, grouping, memoization stability). Typecheck + lint clean.

Refs: docs/plans/conversations-timeline-refactor.md Phase 2

Claude-Session: https://claude.ai/code/session_01MVwyyQVNm8UZpvJyWrf2yo
… additive)

Pure presentational renderer driving off the `TimelineItem[]` projection: one
element per kind, wrapping the existing components. Consecutive process items
(toolCall/subagentActivity) coalesce into a single `ToolTimelineBlock`, matching
today's grouped `agentInsights` anchor; ordering/anchoring/`hideAgentInsights`
filtering stay upstream in the selector. Additive — not yet wired into the panel
(the live render-loop swap is the next, higher-risk step), so no behavior change.

- `timeline/ConversationTimeline.tsx` — kind dispatch + process-run coalescing +
  `onOpenSubagent`/`onViewWholeRun` handler seam.
- `timeline/items/{UserMessageItem,AssistantMessageItem,StreamingTailItem}.tsx`
  — assistant bubbles reuse `splitAgentMessageIntoBubbles` + positions; the
  streaming tail reuses the 120-char ticker slice.

6 renderer tests (message order, process coalescing into one block, subagent
handler wiring, 120-char truncation + thinking details, forked branch tails,
empty timeline). Typecheck + lint clean.

Refs: docs/plans/conversations-timeline-refactor.md Phase 2

Claude-Session: https://claude.ai/code/session_01MVwyyQVNm8UZpvJyWrf2yo
…jection (Phase 2b swap)

Route the live message list through `buildThreadTimeline` — the unified
projection is now the single source of render order. With the streaming/tool
inputs omitted the projection yields exactly the visible messages in order, so
the rendered DOM is unchanged; the tool-timeline block and streaming previews
stay anchored inline below as before. This wires the projection into the
shipping render path ahead of the per-turn grouping in Phase 5.

DOM-identical and verified: the full Conversations render suite + attachments/
overflow/welcomeLock + HumanPage + Accounts (141 tests) stay green, and three
chat Playwright web specs (send+stream, scroll-render, multi-turn history
persistence) pass in a real browser. Typecheck + lint clean.

Note: deeper componentization — rendering messages via <ConversationTimeline>
with the message chrome (attachments/reactions/citations/copy, hideAgentInsights
variants, inference status) ported into item components — remains as follow-up;
it is deferred because a byte-identical port is higher-risk and delivers no
behavior change until Phase 4 stamps real per-turn requestIds.

Refs: docs/plans/conversations-timeline-refactor.md Phase 2

Claude-Session: https://claude.ai/code/session_01MVwyyQVNm8UZpvJyWrf2yo
…timeline by turn (Phase 4)

Rollout step 2 of the per-turn history plan (additive, frontend-only): stamp
`extraMetadata.requestId` on every persisted assistant message so a turn's
answer can be grouped with the process trail that produced it (Option B
anchoring). Covered at all append sites in ChatRuntimeProvider — `chat_done`
(via chatDoneExtraMetadata, incl. the parallel + segment-reconcile paths),
`chat_segment` (merged alongside citations), `chat_interim` narration, and
proactive messages. `addInferenceResponse` already threads extraMetadata to the
persisted message, so this is a caller-side change only.

The timeline projection now derives `turnId` from `extraMetadata.requestId`,
falling back to the `legacy` turn for pre-migration messages — so
`groupTimelineIntoTurns` yields real per-request turns once messages are stamped,
while old threads keep today's single-anchor behavior.

Tests: provider stamps requestId on chat_done; selector derives turnId from
requestId, legacy fallback, and groups a mixed thread into per-request turns
(20 selector + 59 provider tests green). Full Conversations render suite +
typecheck + lint clean.

Refs: docs/plans/conversations-timeline-refactor.md Phase 4;
docs/plans/per-turn-tool-timeline-history.md step 2

Claude-Session: https://claude.ai/code/session_01MVwyyQVNm8UZpvJyWrf2yo
…d, step 1)

Replace the single-snapshot-per-thread layout with a bounded per-turn ring:
`turn_states/<hex(thread_id)>/<hex(request_id)>.json`, one file per turn, so a
multi-turn thread retains every turn's tool timeline instead of only the latest
(the root cause of tab-switch context loss). Completed turns prune to the newest
20 per thread; a live/non-completed turn is never pruned.

Back-compat is preserved so no caller changes are needed: `get(thread_id)` and
`list()` resolve the *latest* turn per thread, `delete`/`clear_all`/
`mark_all_interrupted` operate over the per-thread directory. New
`get_turn(thread_id, request_id)` and `list_thread(thread_id)` expose the
per-turn history. Old flat `<hex(thread_id)>.json` snapshots are migrated in
place on first access (idempotent).

Tests: 14 store tests pass — all 9 preserved (roundtrip, latest wins, delete,
list one-per-thread, mark-interrupted, clear corrupted) plus 5 new (per-turn
retention, get_turn/list_thread, retention prunes completed to N while keeping a
live turn, idempotent legacy migration). Mirror (12) + threads::ops (34) tests
still green.

Refs: docs/plans/conversations-timeline-refactor.md Phase 4;
docs/plans/per-turn-tool-timeline-history.md step 1

Claude-Session: https://claude.ai/code/session_01MVwyyQVNm8UZpvJyWrf2yo
…get_turn (Phase 4, step 3)

Expose the per-turn ring store over JSON-RPC without disturbing the existing
no-arg cold-boot `turn_state_list` or single-turn `turn_state_get`:

- `threads.turn_state_history(threadId)` → every turn snapshot for a thread,
  newest first (the per-turn process history the UI groups by requestId).
- `threads.turn_state_get_turn(threadId, requestId)` → one specific past turn,
  for lazily loading a collapsed turn's full timeline on first expand.

Wires the new `GetTurnStateForRequestRequest` type, ops handlers, controller
schemas (+ registry + parity test), and the `threadApi.getTurnStateHistory` /
`getTurnStateForRequest` clients. Reuses the existing `ListTurnStatesResponse` /
`GetTurnStateResponse` envelopes.

Tests: threads schema parity (17) updated for the two new functions; json_rpc
E2E extended — seeds two turns, asserts history returns both newest-first,
get_turn fetches a specific turn, and an unknown request id returns null. TS
typecheck + threadApi suite (528) green.

Refs: docs/plans/conversations-timeline-refactor.md Phase 4;
docs/plans/per-turn-tool-timeline-history.md step 3

Claude-Session: https://claude.ai/code/session_01MVwyyQVNm8UZpvJyWrf2yo
…se 5)

Each past answer now keeps its own "agent insights" trail instead of losing it
on the next send — the tab-switch context-loss fix made visible.

- New `chatRuntimeSlice.turnTimelinesByThread` (threadId → requestId → entries)
  holds the *settled* turns' timelines, hydrated by `fetchAndHydrateTurnHistory`
  from the `turn_state_history` RPC on thread open. It deliberately survives a
  new send (unlike `toolTimelineByThread`), and excludes the newest turn (that
  one still renders as the live/anchored insights via `getTurnState`), so no
  turn double-renders.
- The render loop maps each older turn's timeline to the first assistant message
  of its `requestId` group and renders a collapsed `ToolTimelineBlock` above it.
  Purely additive: threads without hydrated history (legacy messages, existing
  tests) render exactly as before.

Tests: thunk stores older settled turns by requestId while skipping the latest
and filtering non-settled lifecycles, and swallows transport errors; render test
asserts a past turn's trail appears once above its answer only after hydration.
Full Conversations render suite (68) + slice/provider/thunk suites (120) green;
typecheck + lint clean.

Refs: docs/plans/conversations-timeline-refactor.md Phase 5;
docs/plans/per-turn-tool-timeline-history.md step 4

Claude-Session: https://claude.ai/code/session_01MVwyyQVNm8UZpvJyWrf2yo
Apply Prettier and cargo fmt to the files touched across Phases 1–5 so the
CI format gate (pnpm format:check) passes. No behavior change.

Claude-Session: https://claude.ai/code/session_01MVwyyQVNm8UZpvJyWrf2yo
@senamakel senamakel requested a review from a team July 6, 2026 23:19
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 40 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: afde718f-a4e9-4c8d-8e45-84af51c4e680

📥 Commits

Reviewing files that changed from the base of the PR and between dbe4e32 and 9ec5dee.

📒 Files selected for processing (11)
  • app/src/features/conversations/Conversations.tsx
  • app/src/features/conversations/threadList/ThreadList.tsx
  • app/src/features/human/HumanPage.test.tsx
  • app/src/features/human/HumanPage.tsx
  • app/src/pages/Accounts.tsx
  • app/src/pages/__tests__/Accounts.webviewSelection.test.tsx
  • app/src/pages/__tests__/Conversations.attachments.test.tsx
  • app/src/pages/__tests__/Conversations.render.test.tsx
  • app/src/pages/__tests__/Conversations.sidebarOverflow.test.tsx
  • app/src/pages/__tests__/Conversations.test.tsx
  • app/src/pages/__tests__/Conversations.welcomeLock.test.tsx
📝 Walkthrough

Walkthrough

This PR relocates conversation-related components/utilities from pages/conversations to a new features/conversations module, introduces a unified TimelineItem projection model with selectors and rendering components, adds turn-history hydration wiring, and reworks the Rust TurnStateStore to a per-turn directory layout with new RPC endpoints for turn history and per-request lookup.

Changes

Frontend Conversations Timeline Feature

Layer / File(s) Summary
Import path migration
app/src/components/channels/mcp/ConfigAssistantPanel.tsx, app/src/components/chat/CycleUsagePill.tsx, app/src/components/flows/*, app/src/components/intelligence/*, app/src/components/layout/shell/useNewChat.ts, app/src/components/notifications/NotificationBody.tsx, app/src/pages/dev/AgentInsightsPreview.tsx
Import specifiers/comments updated from pages/conversations to features/conversations locations; test mocks updated to match.
Barrel export and Accounts wiring
app/src/features/conversations/index.ts, app/src/pages/Accounts.tsx, app/src/pages/__tests__/Accounts.webviewSelection.test.tsx
New barrel export module re-exports Conversations/ConversationsPage and helpers; Accounts mounts ConversationsPage instead of AgentChatPanel.
Timeline data model
app/src/features/conversations/timeline/types.ts
Adds TimelineItem union, TimelineItemBase, TimelineTurn, AGENT_INSIGHT_KINDS, and helper functions.
Timeline projection selectors
app/src/features/conversations/timeline/selectors.ts, selectors.test.ts
Adds buildThreadTimeline, groupTimelineIntoTurns, and memoized selectTimelineForThread, with unit tests.
Timeline rendering components
app/src/features/conversations/timeline/ConversationTimeline.tsx, items/*, ConversationTimeline.test.tsx
Adds ConversationTimeline (coalesces process items into ToolTimelineBlock) and item renderers UserMessageItem, AssistantMessageItem, StreamingTailItem, with tests.
Turn-history hydration wiring
app/src/services/api/threadApi.ts, app/src/store/chatRuntimeSlice.ts, app/src/providers/ChatRuntimeProvider.tsx, associated tests
Adds getTurnStateHistory/getTurnStateForRequest, turnTimelinesByThread state and fetchAndHydrateTurnHistory thunk, and requestId stamping in extraMetadata.

Backend Per-Turn State Persistence

Layer / File(s) Summary
Per-turn store layout and pruning
src/openhuman/threads/turn_state/store.rs, store_tests.rs
Reworks TurnStateStore to a per-turn directory layout with COMPLETED_RETENTION pruning, legacy migration, and new get_turn/list_thread APIs.
Turn state types
src/openhuman/threads/turn_state/types.rs, mod.rs
Adds GetTurnStateForRequestRequest and updates re-exports/docs.
RPC endpoints
src/openhuman/threads/ops.rs, schemas.rs, schemas_tests.rs, tests/json_rpc_e2e.rs
Adds turn_state_history/turn_state_get_turn RPC handlers, schema registrations, and e2e coverage.
Refactor plan documentation
docs/plans/conversations-timeline-refactor.md
Adds a phased plan document describing the timeline refactor architecture and rollout.

Estimated code review effort: 4 (Complex) | ~75 minutes

Possibly related PRs

  • tinyhumansai/openhuman#1202: Shares the same turn-state RPC methods (threadApi) and conversation runtime hydration logic (chatRuntimeSlice/Conversations.tsx, ops.rs/schemas) as the persistence foundation this PR builds timeline features on.

Suggested labels: feature, rust-core, working

Suggested reviewers: graycyrus, M3gA-Mind

Poem

A rabbit hopped through folders deep,
from pages to features did leap,
each turn now snug in its own file,
stacked neat in a per-turn pile. 🥕
Timeline hums, no burrow lost —
hop on, review, at modest cost! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: per-turn process history in the conversations timeline refactor.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot added feature Net-new user-facing capability or product behavior. rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. working A PR that is being worked on by the team. labels Jul 6, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dbe4e326de

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

for (const turn of history.slice(1)) {
if (turn.lifecycle !== 'completed' && turn.lifecycle !== 'interrupted') continue;
if (!turn.requestId || turn.toolTimeline.length === 0) continue;
timelines[turn.requestId] = turn.toolTimeline.map(toolTimelineFromPersisted);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Settle interrupted historical tool rows

When an older turn is interrupted (which this history path explicitly includes), its persisted snapshot can still contain tool/subagent rows with status: 'running' because the live driver died before emitting completion events. Hydrating those rows directly makes a reopened thread render stale past-turn insights as still running/pulsing, whereas the latest-snapshot path already applies settleOrphanedTimelineEntry; apply the same settling for interrupted history before storing these timelines.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/store/chatRuntimeSlice.ts (1)

1408-1426: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

clearRuntimeForThread doesn't clear turnTimelinesByThread, leaking per-thread state on delete/reset.

clearAllChatRuntime resets turnTimelinesByThread globally, but the per-thread clearRuntimeForThread reducer (which clears every other per-thread map, e.g. toolTimelineByThread, processingByThread, taskBoardByThread) omits it. Unlike artifactsByThread, which has an explicit comment justifying its exclusion, this omission appears unintentional. Once a thread is deleted, its turnTimelinesByThread[threadId] entry is never freed.

🛠️ Proposed fix
     clearRuntimeForThread: (state, action: PayloadAction<{ threadId: string }>) => {
       delete state.inferenceStatusByThread[action.payload.threadId];
       delete state.streamingAssistantByThread[action.payload.threadId];
       delete state.inferenceHeartbeatByThread[action.payload.threadId];
       ...
       delete state.toolTimelineByThread[action.payload.threadId];
+      delete state.turnTimelinesByThread[action.payload.threadId];
       delete state.processingByThread[action.payload.threadId];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/store/chatRuntimeSlice.ts` around lines 1408 - 1426,
clearRuntimeForThread is missing cleanup for turnTimelinesByThread, leaving
per-thread timeline state behind after thread delete/reset. Update the
clearRuntimeForThread reducer in chatRuntimeSlice to delete the matching
turnTimelinesByThread[threadId] entry alongside the other per-thread maps
already being cleared, following the same pattern used for toolTimelineByThread,
processingByThread, and taskBoardByThread; keep any intentional exclusions like
artifactsByThread unchanged.
🧹 Nitpick comments (7)
app/src/features/conversations/timeline/items/AssistantMessageItem.tsx (1)

34-38: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unnecessary Fragment wrapping a single child.

<Fragment key={index}> wrapping one <AgentMessageBubble> adds no value here — the key can go directly on AgentMessageBubble.

♻️ Proposed simplification
-        return (
-          <Fragment key={index}>
-            <AgentMessageBubble content={segment} position={position} />
-          </Fragment>
-        );
+        return <AgentMessageBubble key={index} content={segment} position={position} />;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/features/conversations/timeline/items/AssistantMessageItem.tsx`
around lines 34 - 38, The map render in AssistantMessageItem is wrapping a
single AgentMessageBubble in a Fragment just to carry the key, which is
unnecessary. Remove the Fragment and put the key directly on AgentMessageBubble
inside the segment rendering logic so the structure is simpler while preserving
list identity.
app/src/features/conversations/timeline/ConversationTimeline.test.tsx (2)

94-108: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Fragile CSS-class selector for asserting the content bubble.

container.querySelector('.font-mono.text-sm') (Line 106) couples the test to StreamingTailItem's exact Tailwind classes; a future style tweak would silently break this assertion without indicating an actual behavior regression. Consider adding a data-testid on the content wrapper instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/features/conversations/timeline/ConversationTimeline.test.tsx` around
lines 94 - 108, The test in ConversationTimeline.test.tsx is using a fragile
CSS-class query to locate the streaming content bubble, which ties it to
StreamingTailItem’s Tailwind styling instead of behavior. Update the component
to expose a stable test hook such as a data-testid on the content wrapper, then
change the assertion in the ConversationTimeline test to target that identifier
rather than .font-mono.text-sm.

1-13: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Tests use the real production store instead of a test-scoped store.

renderInStore wraps components with the actual app store (Line 5, 12) rather than a minimal test store built from testRootReducer. This works for now since no state is read, but it couples this test's stability to whatever middleware/side effects the real store initializes (sockets, persistence, etc.), and any future selector usage inside these components would read live default state rather than controlled fixtures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/features/conversations/timeline/ConversationTimeline.test.tsx` around
lines 1 - 13, The test helper renderInStore is using the real app store instead
of an isolated test store, which makes ConversationTimeline.test.tsx depend on
production middleware and default state. Replace the imported store with a
test-scoped store created from testRootReducer (or an equivalent minimal test
setup) inside renderInStore, and keep ConversationTimeline and
buildThreadTimeline tests wired to controlled fixture state so the test remains
deterministic.
app/src/features/conversations/timeline/ConversationTimeline.tsx (1)

41-63: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Coalescing loop and process-item mapping look correct.

Anchoring/ordering logic is delegated upstream as documented, and this component purely projects; the cast on Line 50 is redundant given the isProcessItem guard on the while condition but harmless.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/features/conversations/timeline/ConversationTimeline.tsx` around
lines 41 - 63, The coalescing logic in ConversationTimeline and the
ToolTimelineBlock rendering are already correct, so no behavioral change is
needed; the only cleanup is to remove the redundant type cast inside the while
loop where isProcessItem already narrows items[j]. Keep the existing run
collection, keying by startId, and upstream ordering assumptions intact.
app/src/providers/ChatRuntimeProvider.tsx (1)

1046-1053: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add debug logging for the new requestId stamping.

onSegment, onInterim, and onProactiveMessage now stamp requestId onto persisted messages — the core signal the new per-turn timeline grouping relies on — but none of them log it. If request_id is ever missing/empty here, there's currently no diagnostic trail to explain a turn silently failing to group with its process trail.

As per path instructions: "On new or changed flows, add verbose debug logging with stable prefixes, correlation fields, and no secrets or full PII; incomplete changes are those lacking logging."

Also applies to: 1068-1076, 1238-1245

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/providers/ChatRuntimeProvider.tsx` around lines 1046 - 1053, Add
verbose debug logging around the new requestId stamping in ChatRuntimeProvider
so missing or empty request_id values can be diagnosed; update the onSegment,
onInterim, and onProactiveMessage flows that build extraMetadata to log a stable
debug prefix plus correlation fields (such as turn/message identifiers and
whether requestId was stamped) without secrets or full PII. Use the existing
requestId/request_id handling in ChatRuntimeProvider to log both the success
path when extraMetadata includes requestId and the fallback path when it is
absent, so failures to group a turn with its process trail are traceable.

Source: Path instructions

src/openhuman/threads/turn_state/store.rs (2)

261-434: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

migrate_all_legacy_locked silently drops write failures.

migrate_thread_locked logs a warn! when write_turn_file fails (line 376-379). migrate_all_legacy_locked's equivalent branch (if self.write_turn_file(&state).is_ok() { ... }) does nothing at all when it returns Err — no log, no trace. A legacy file that repeatedly fails to migrate during a bulk scan will be silently retried forever with zero diagnostic signal, unlike the per-thread path.

🔧 Proposed fix for parity with `migrate_thread_locked`
             match read_snapshot(&path) {
                 Ok(state) => {
-                    if self.write_turn_file(&state).is_ok() {
-                        let _ = fs::remove_file(&path);
-                        debug!(
-                            "{LOG_PREFIX} migrated legacy snapshot thread={} request={}",
-                            state.thread_id, state.request_id
-                        );
-                    }
+                    match self.write_turn_file(&state) {
+                        Ok(()) => {
+                            if let Err(err) = fs::remove_file(&path) {
+                                warn!(
+                                    "{LOG_PREFIX} migrate: removed-into-dir but flat delete failed {}: {err}",
+                                    path.display()
+                                );
+                            } else {
+                                debug!(
+                                    "{LOG_PREFIX} migrated legacy snapshot thread={} request={}",
+                                    state.thread_id, state.request_id
+                                );
+                            }
+                        }
+                        Err(err) => warn!(
+                            "{LOG_PREFIX} legacy migrate write failed {}: {err} (flat file kept)",
+                            path.display()
+                        ),
+                    }
                 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/threads/turn_state/store.rs` around lines 261 - 434, The bulk
migration path in migrate_all_legacy_locked is swallowing write errors when
write_turn_file fails, unlike migrate_thread_locked which logs a warn! for the
same failure. Update migrate_all_legacy_locked to handle the Err case from
write_turn_file explicitly and emit a warning with the legacy file path and
error details, keeping behavior and diagnostics consistent with
migrate_thread_locked.

143-173: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Cold-boot list() now reads/parses every retained turn per thread just to pick the latest.

Previously each thread had exactly one flat snapshot file, so list() was O(threads). Now read_thread_turns (called from list() and mark_all_interrupted()) opens and fully deserializes up to COMPLETED_RETENTION + 1 (21) turn files per thread just to compute latest_turn, making both cold-boot paths O(threads × 21). For workspaces with many long-lived threads this meaningfully increases startup I/O/CPU versus the prior single-file-per-thread design, and both of these cold-boot paths (list() and mark_all_interrupted()) do this scan independently, doubling the cost.

A cheaper design (e.g., a small per-thread "latest pointer" file, or filename-embedded timestamps enabling a metadata-only comparison before parsing) would let get/list avoid parsing every retained completed turn just to find the newest one.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/threads/turn_state/store.rs` around lines 143 - 173, The
cold-boot path in TurnStateStore::list is fully deserializing every retained
turn via read_thread_turns just to compute latest_turn, which makes startup
unnecessarily O(threads × retention). Update the lookup so list() and
mark_all_interrupted() can identify the newest turn without parsing every file,
for example by using a per-thread latest pointer or metadata-based ordering,
while keeping list_thread() as the full sorted read for one thread.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/features/conversations/timeline/items/StreamingTailItem.tsx`:
- Line 24: The StreamingTailItem component is rendering a hardcoded English
label instead of using i18n. Update the summary text in StreamingTailItem to
come from useT() in app/src/lib/i18n/I18nContext, add a new translation key in
the English locale, and mirror that key across all locale files so the
"Thinking…" label is translated consistently.

In `@docs/plans/conversations-timeline-refactor.md`:
- Around line 56-75: The markdown fence for the directory-tree block is missing
a language label, causing lint issues; update the fenced block in the
conversations timeline refactor plan to use a text/plaintext tag so it remains a
valid documentation tree snippet. Make the change directly in the directory-tree
section near the `app/src/features/conversations/` and
`app/src/pages/Conversations.tsx` listing, keeping the content unchanged aside
from the fence label.

In `@src/openhuman/threads/turn_state/store.rs`:
- Around line 100-118: get_turn currently treats a bad turn snapshot as a hard
error, unlike get/list/list_thread which skip unreadable files. Update
Store::get_turn to mirror read_thread_turns by catching read_snapshot failures,
logging a warning, and returning Ok(None) for corrupted/legacy snapshots so
ops::turn_state_get_turn does not bubble an RPC error. Add or update a
store_tests.rs case that writes an invalid turn file and verifies get_turn
returns None instead of Err.

---

Outside diff comments:
In `@app/src/store/chatRuntimeSlice.ts`:
- Around line 1408-1426: clearRuntimeForThread is missing cleanup for
turnTimelinesByThread, leaving per-thread timeline state behind after thread
delete/reset. Update the clearRuntimeForThread reducer in chatRuntimeSlice to
delete the matching turnTimelinesByThread[threadId] entry alongside the other
per-thread maps already being cleared, following the same pattern used for
toolTimelineByThread, processingByThread, and taskBoardByThread; keep any
intentional exclusions like artifactsByThread unchanged.

---

Nitpick comments:
In `@app/src/features/conversations/timeline/ConversationTimeline.test.tsx`:
- Around line 94-108: The test in ConversationTimeline.test.tsx is using a
fragile CSS-class query to locate the streaming content bubble, which ties it to
StreamingTailItem’s Tailwind styling instead of behavior. Update the component
to expose a stable test hook such as a data-testid on the content wrapper, then
change the assertion in the ConversationTimeline test to target that identifier
rather than .font-mono.text-sm.
- Around line 1-13: The test helper renderInStore is using the real app store
instead of an isolated test store, which makes ConversationTimeline.test.tsx
depend on production middleware and default state. Replace the imported store
with a test-scoped store created from testRootReducer (or an equivalent minimal
test setup) inside renderInStore, and keep ConversationTimeline and
buildThreadTimeline tests wired to controlled fixture state so the test remains
deterministic.

In `@app/src/features/conversations/timeline/ConversationTimeline.tsx`:
- Around line 41-63: The coalescing logic in ConversationTimeline and the
ToolTimelineBlock rendering are already correct, so no behavioral change is
needed; the only cleanup is to remove the redundant type cast inside the while
loop where isProcessItem already narrows items[j]. Keep the existing run
collection, keying by startId, and upstream ordering assumptions intact.

In `@app/src/features/conversations/timeline/items/AssistantMessageItem.tsx`:
- Around line 34-38: The map render in AssistantMessageItem is wrapping a single
AgentMessageBubble in a Fragment just to carry the key, which is unnecessary.
Remove the Fragment and put the key directly on AgentMessageBubble inside the
segment rendering logic so the structure is simpler while preserving list
identity.

In `@app/src/providers/ChatRuntimeProvider.tsx`:
- Around line 1046-1053: Add verbose debug logging around the new requestId
stamping in ChatRuntimeProvider so missing or empty request_id values can be
diagnosed; update the onSegment, onInterim, and onProactiveMessage flows that
build extraMetadata to log a stable debug prefix plus correlation fields (such
as turn/message identifiers and whether requestId was stamped) without secrets
or full PII. Use the existing requestId/request_id handling in
ChatRuntimeProvider to log both the success path when extraMetadata includes
requestId and the fallback path when it is absent, so failures to group a turn
with its process trail are traceable.

In `@src/openhuman/threads/turn_state/store.rs`:
- Around line 261-434: The bulk migration path in migrate_all_legacy_locked is
swallowing write errors when write_turn_file fails, unlike migrate_thread_locked
which logs a warn! for the same failure. Update migrate_all_legacy_locked to
handle the Err case from write_turn_file explicitly and emit a warning with the
legacy file path and error details, keeping behavior and diagnostics consistent
with migrate_thread_locked.
- Around line 143-173: The cold-boot path in TurnStateStore::list is fully
deserializing every retained turn via read_thread_turns just to compute
latest_turn, which makes startup unnecessarily O(threads × retention). Update
the lookup so list() and mark_all_interrupted() can identify the newest turn
without parsing every file, for example by using a per-thread latest pointer or
metadata-based ordering, while keeping list_thread() as the full sorted read for
one thread.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4b1866ef-a53c-44d5-8433-eeae25f2dacf

📥 Commits

Reviewing files that changed from the base of the PR and between 8509c8c and dbe4e32.

📒 Files selected for processing (79)
  • app/src/components/channels/mcp/ConfigAssistantPanel.tsx
  • app/src/components/chat/CycleUsagePill.tsx
  • app/src/components/flows/FlowRunInspectorDrawer.tsx
  • app/src/components/flows/WorkflowCopilotPanel.tsx
  • app/src/components/intelligence/IntelligenceTasksTab.tsx
  • app/src/components/intelligence/ModelCouncilTab.tsx
  • app/src/components/intelligence/__tests__/IntelligenceTasksTab.test.tsx
  • app/src/components/layout/shell/useNewChat.ts
  • app/src/components/notifications/NotificationBody.tsx
  • app/src/features/conversations/Conversations.tsx
  • app/src/features/conversations/components/AgentMessageBubble.test.tsx
  • app/src/features/conversations/components/AgentMessageBubble.tsx
  • app/src/features/conversations/components/AgentProcessSourcePanel.tsx
  • app/src/features/conversations/components/AgentTimelineRail.tsx
  • app/src/features/conversations/components/BackgroundActivityRows.tsx
  • app/src/features/conversations/components/BackgroundProcessesPanel.tsx
  • app/src/features/conversations/components/CitationChips.tsx
  • app/src/features/conversations/components/LimitPill.tsx
  • app/src/features/conversations/components/PlanReviewCard.test.tsx
  • app/src/features/conversations/components/PlanReviewCard.tsx
  • app/src/features/conversations/components/ProcessingTranscriptView.test.tsx
  • app/src/features/conversations/components/ProcessingTranscriptView.tsx
  • app/src/features/conversations/components/SubagentDrawer.tsx
  • app/src/features/conversations/components/TaskKanbanBoard.test.tsx
  • app/src/features/conversations/components/TaskKanbanBoard.tsx
  • app/src/features/conversations/components/ThreadGoalChip.test.tsx
  • app/src/features/conversations/components/ThreadGoalChip.tsx
  • app/src/features/conversations/components/ThreadTodoStrip.test.tsx
  • app/src/features/conversations/components/ThreadTodoStrip.tsx
  • app/src/features/conversations/components/ToolFailureLines.tsx
  • app/src/features/conversations/components/ToolTimelineBlock.tsx
  • app/src/features/conversations/components/WorkerThreadRefCard.tsx
  • app/src/features/conversations/components/__tests__/AgentProcessSourcePanel.test.tsx
  • app/src/features/conversations/components/__tests__/AgentTimelineRail.test.tsx
  • app/src/features/conversations/components/__tests__/BackgroundActivityRows.test.tsx
  • app/src/features/conversations/components/__tests__/BackgroundProcessesPanel.test.tsx
  • app/src/features/conversations/components/__tests__/SubagentDrawer.test.tsx
  • app/src/features/conversations/components/__tests__/TaskKanbanBoard.test.tsx
  • app/src/features/conversations/components/__tests__/ToolTimelineBlock.test.tsx
  • app/src/features/conversations/components/__tests__/WorkerThreadRefCard.test.tsx
  • app/src/features/conversations/composerSendDecision.test.ts
  • app/src/features/conversations/composerSendDecision.ts
  • app/src/features/conversations/hooks/useBackgroundActivity.test.ts
  • app/src/features/conversations/hooks/useBackgroundActivity.ts
  • app/src/features/conversations/index.ts
  • app/src/features/conversations/taskPlanActions.test.ts
  • app/src/features/conversations/taskPlanActions.ts
  • app/src/features/conversations/timeline/ConversationTimeline.test.tsx
  • app/src/features/conversations/timeline/ConversationTimeline.tsx
  • app/src/features/conversations/timeline/items/AssistantMessageItem.tsx
  • app/src/features/conversations/timeline/items/StreamingTailItem.tsx
  • app/src/features/conversations/timeline/items/UserMessageItem.tsx
  • app/src/features/conversations/timeline/selectors.test.ts
  • app/src/features/conversations/timeline/selectors.ts
  • app/src/features/conversations/timeline/types.ts
  • app/src/features/conversations/utils/format.ts
  • app/src/features/conversations/utils/threadFilter.test.ts
  • app/src/features/conversations/utils/threadFilter.ts
  • app/src/features/conversations/utils/workerThreadRef.test.ts
  • app/src/features/conversations/utils/workerThreadRef.ts
  • app/src/pages/Accounts.tsx
  • app/src/pages/Conversations.tsx
  • app/src/pages/__tests__/Accounts.webviewSelection.test.tsx
  • app/src/pages/__tests__/Conversations.render.test.tsx
  • app/src/pages/dev/AgentInsightsPreview.tsx
  • app/src/providers/ChatRuntimeProvider.tsx
  • app/src/providers/__tests__/ChatRuntimeProvider.test.tsx
  • app/src/services/api/threadApi.ts
  • app/src/store/__tests__/chatRuntimeSlice.thunk.test.ts
  • app/src/store/chatRuntimeSlice.ts
  • docs/plans/conversations-timeline-refactor.md
  • src/openhuman/threads/ops.rs
  • src/openhuman/threads/schemas.rs
  • src/openhuman/threads/schemas_tests.rs
  • src/openhuman/threads/turn_state/mod.rs
  • src/openhuman/threads/turn_state/store.rs
  • src/openhuman/threads/turn_state/store_tests.rs
  • src/openhuman/threads/turn_state/types.rs
  • tests/json_rpc_e2e.rs

<div className="max-w-[80%] space-y-1">
{thinking && thinking.length > 0 ? (
<details className="text-xs text-content-muted">
<summary className="cursor-pointer select-none">Thinking…</summary>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Hardcoded English string bypasses i18n.

"Thinking…" is rendered directly instead of going through useT(). As per coding guidelines, "All UI text must go through useT() from app/src/lib/i18n/I18nContext, and new keys must be added to en.ts plus all locale files (ar, bn, de, es, fr, hi, id, it, ko, pl, pt, ru, zh-CN)."

🌐 Proposed fix
+import { useT } from '../../../../lib/i18n/I18nContext';
+
 export function StreamingTailItem({
   text,
   thinking,
   branch = false,
 }: {
   text: string;
   thinking?: string;
   branch?: boolean;
 }) {
+  const { t } = useT();
   const tail = text.slice(-STREAMING_PREVIEW_CHARS);
@@
-          <summary className="cursor-pointer select-none">Thinking…</summary>
+          <summary className="cursor-pointer select-none">{t('conversations.streamingTail.thinking')}</summary>

Add the new key to en.ts and all other locale files.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<summary className="cursor-pointer select-none">Thinking…</summary>
import { useT } from '../../../../lib/i18n/I18nContext';
export function StreamingTailItem({
text,
thinking,
branch = false,
}: {
text: string;
thinking?: string;
branch?: boolean;
}) {
const { t } = useT();
const tail = text.slice(-STREAMING_PREVIEW_CHARS);
...
<summary className="cursor-pointer select-none">{t('conversations.streamingTail.thinking')}</summary>
...
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/features/conversations/timeline/items/StreamingTailItem.tsx` at line
24, The StreamingTailItem component is rendering a hardcoded English label
instead of using i18n. Update the summary text in StreamingTailItem to come from
useT() in app/src/lib/i18n/I18nContext, add a new translation key in the English
locale, and mirror that key across all locale files so the "Thinking…" label is
translated consistently.

Source: Path instructions

Comment on lines +56 to +75
```
app/src/features/conversations/
ConversationsPage.tsx # page-shell variant
ConversationsSidebar.tsx # sidebar variant (Accounts, HumanPage)
index.ts # public exports = reusability contract
timeline/
types.ts # TimelineItem union
selectors.ts # projection + turn grouping (reselect-memoized)
ConversationTimeline.tsx # pure renderer: TimelineItem[] → one component per kind
items/ # UserMessageItem, AssistantMessageItem, ToolCallItem,
# ReasoningItem, SubagentActivityItem, StreamingTailItem
TurnInsightsGroup.tsx # collapsed-header/expanded-body per-turn wrapper (lazy-load)
composer/
Composer.tsx / MicCloudComposer.tsx / useComposerState.ts / composerSendDecision.ts
threadList/
ThreadList.tsx / threadFilter.ts
components/ # existing subcomponents relocated as-is
(ToolTimelineBlock, SubagentDrawer, TaskKanbanBoard, AgentProcessSourcePanel, …)
app/src/pages/Conversations.tsx # thin re-export shim during migration; deleted in Phase 6
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Label the directory-tree fence.

markdownlint flags this block because it has no language tag. Adding text/plaintext keeps the doc lint-clean and renders more consistently.

Suggested fix
-```
+```text
 app/src/features/conversations/
   ConversationsPage.tsx          # page-shell variant
   ConversationsSidebar.tsx       # sidebar variant (Accounts, HumanPage)
@@
 app/src/pages/Conversations.tsx  # thin re-export shim during migration; deleted in Phase 6

</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 56-56: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/plans/conversations-timeline-refactor.md` around lines 56 - 75, The
markdown fence for the directory-tree block is missing a language label, causing
lint issues; update the fenced block in the conversations timeline refactor plan
to use a text/plaintext tag so it remains a valid documentation tree snippet.
Make the change directly in the directory-tree section near the
`app/src/features/conversations/` and `app/src/pages/Conversations.tsx` listing,
keeping the content unchanged aside from the fence label.

Source: Linters/SAST tools

Comment on lines +100 to 118
/// Return the latest turn for `thread_id`, or `None` if none exists.
/// "Latest" is the turn with the greatest `started_at` (ties broken by
/// `updated_at`) — the in-flight or most-recent turn.
pub fn get(&self, thread_id: &str) -> Result<Option<TurnState>, String> {
let _guard = TURN_STATE_LOCK.lock();
let path = self.snapshot_path(thread_id);
self.migrate_thread_locked(thread_id);
Ok(latest_turn(self.read_thread_turns(thread_id)?))
}

/// Return a specific turn by `request_id`, or `None` if absent.
pub fn get_turn(&self, thread_id: &str, request_id: &str) -> Result<Option<TurnState>, String> {
let _guard = TURN_STATE_LOCK.lock();
self.migrate_thread_locked(thread_id);
let path = self.turn_path(thread_id, request_id);
if !path.exists() {
return Ok(None);
}
read_snapshot(&path).map(Some)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

get_turn fails hard on an unreadable snapshot instead of degrading gracefully.

Every other read path (get, list, list_thread, via read_thread_turns) logs a warning and skips an unparsable turn file. get_turn instead calls read_snapshot(&path) directly and propagates any parse error as Err(String), which ops::turn_state_get_turn bubbles up via ? into a JSON-RPC error — turning a single corrupted/legacy turn file into a hard RPC failure for the new "lazily load a past turn" UI flow, instead of the None/null the UI already handles.

🐛 Proposed fix to align with sibling read paths
     pub fn get_turn(&self, thread_id: &str, request_id: &str) -> Result<Option<TurnState>, String> {
         let _guard = TURN_STATE_LOCK.lock();
         self.migrate_thread_locked(thread_id);
         let path = self.turn_path(thread_id, request_id);
         if !path.exists() {
             return Ok(None);
         }
-        read_snapshot(&path).map(Some)
+        match read_snapshot(&path) {
+            Ok(snapshot) => Ok(Some(snapshot)),
+            Err(err) => {
+                warn!(
+                    "{LOG_PREFIX} skip unreadable snapshot {}: {err}",
+                    path.display()
+                );
+                Ok(None)
+            }
+        }

Consider adding a store_tests.rs case that hand-writes a corrupted turn file and asserts get_turn returns Ok(None) rather than Err.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Return the latest turn for `thread_id`, or `None` if none exists.
/// "Latest" is the turn with the greatest `started_at` (ties broken by
/// `updated_at`) — the in-flight or most-recent turn.
pub fn get(&self, thread_id: &str) -> Result<Option<TurnState>, String> {
let _guard = TURN_STATE_LOCK.lock();
let path = self.snapshot_path(thread_id);
self.migrate_thread_locked(thread_id);
Ok(latest_turn(self.read_thread_turns(thread_id)?))
}
/// Return a specific turn by `request_id`, or `None` if absent.
pub fn get_turn(&self, thread_id: &str, request_id: &str) -> Result<Option<TurnState>, String> {
let _guard = TURN_STATE_LOCK.lock();
self.migrate_thread_locked(thread_id);
let path = self.turn_path(thread_id, request_id);
if !path.exists() {
return Ok(None);
}
read_snapshot(&path).map(Some)
}
/// Return the latest turn for `thread_id`, or `None` if none exists.
/// "Latest" is the turn with the greatest `started_at` (ties broken by
/// `updated_at`) — the in-flight or most-recent turn.
pub fn get(&self, thread_id: &str) -> Result<Option<TurnState>, String> {
let _guard = TURN_STATE_LOCK.lock();
self.migrate_thread_locked(thread_id);
Ok(latest_turn(self.read_thread_turns(thread_id)?))
}
/// Return a specific turn by `request_id`, or `None` if absent.
pub fn get_turn(&self, thread_id: &str, request_id: &str) -> Result<Option<TurnState>, String> {
let _guard = TURN_STATE_LOCK.lock();
self.migrate_thread_locked(thread_id);
let path = self.turn_path(thread_id, request_id);
if !path.exists() {
return Ok(None);
}
match read_snapshot(&path) {
Ok(snapshot) => Ok(Some(snapshot)),
Err(err) => {
warn!(
"{LOG_PREFIX} skip unreadable snapshot {}: {err}",
path.display()
);
Ok(None)
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/threads/turn_state/store.rs` around lines 100 - 118, get_turn
currently treats a bad turn snapshot as a hard error, unlike
get/list/list_thread which skip unreadable files. Update Store::get_turn to
mirror read_thread_turns by catching read_snapshot failures, logging a warning,
and returning Ok(None) for corrupted/legacy snapshots so
ops::turn_state_get_turn does not bubble an RPC error. Add or update a
store_tests.rs case that writes an invalid turn file and verifies get_turn
returns None instead of Err.

senamakel added 3 commits July 6, 2026 16:31
…, partial)

The panel has lived at `features/conversations/Conversations.tsx` since Phase 1b;
remove the temporary `pages/Conversations.tsx` re-export shim and point every
consumer at the real module: `HumanPage`, `Accounts` (+ its `ConversationsPage`
alias), and the co-located Conversations/Accounts test suites and mocks.

Scope note: Phase 6's dead-state removal (`parallelStreamsByThread`,
`preserveLiveSubagentProse`, provider refs) is coupled to Phase 3 and is tracked
in the follow-up issue, not this PR.

Typecheck + lint clean; 119 tests across the Conversations render suite,
Accounts, and HumanPage pass with the repointed imports.

Refs: docs/plans/conversations-timeline-refactor.md Phase 6

Claude-Session: https://claude.ai/code/session_01MVwyyQVNm8UZpvJyWrf2yo
…hells)

Move the left-rail thread list (search, new-conversation row, thread rows with
inline rename + delete) out of the 3.3k-line panel into a self-contained
presentational `features/conversations/threadList/ThreadList.tsx`. Driven by a
clean callback interface — `onSelectThread` / `onRequestDelete` /
`onCommitTitle` / `onBlurTitle` etc. encapsulate the multi-step dispatch+route
logic in the parent, so the component takes ~14 typed props instead of drilling
dispatch/navigate/refs. JSX moved verbatim; no behavior change. The panel shrinks
by ~165 lines.

Guardrails: full Conversations render suite + sidebarOverflow (thread search,
row select, inline rename, delete modal via existing data-testids), Accounts,
and HumanPage all pass. Typecheck + lint clean.

Refs: docs/plans/conversations-timeline-refactor.md Phase 1 (shell extraction)

Claude-Session: https://claude.ai/code/session_01MVwyyQVNm8UZpvJyWrf2yo
eslint import/order auto-fix for the HumanPage/Accounts imports repointed to
features/conversations in the shim-deletion commit. No behavior change.

Claude-Session: https://claude.ai/code/session_01MVwyyQVNm8UZpvJyWrf2yo

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9ec5dee94c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// hydrates into `toolTimelineByThread` (rendered as the live/anchored
// "agent insights"), so skip it here to avoid rendering it twice — this
// field holds only the *older* settled turns.
for (const turn of history.slice(1)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve previous turn insights before starting the next turn

When the user keeps a thread open, the newest completed turn is skipped here and exists only in toolTimelineByThread; the next send immediately clears that live timeline with setToolTimelineForThread([]) before this history thunk runs again. That means the just-finished answer loses its process trail as soon as the user starts another turn, and it only comes back after switching/reopening the thread. Stash the skipped completed turn when a newer request starts/completes, or refresh history at turn end, instead of permanently leaving it out of turnTimelinesByThread.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Net-new user-facing capability or product behavior. rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. working A PR that is being worked on by the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant