You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Complete the remaining phases of the conversations timeline refactor that were deliberately deferred out of #4612 (which landed Phases 1–2 and 4–5 — the per-turn process history feature end-to-end). Three follow-up bodies of work remain, in priority order:
#4612 delivered the user-visible feature (each answer keeps its own tool-timeline trail) but intentionally stopped short of the internal-robustness refactor. ChatRuntimeProvider.tsx (~1,664 lines) still merges ~28 snake_case socket events via store.getState() + find-row + full-array-replace, using two parallel dedup mechanisms with divergent row-id schemes, plus the preserveLiveSubagentProse graft (a documented live-only-state workaround). This is the "buggy in how it stores & streams" dimension from the audit.
Why it was deferred: Phase 3 rewrites the live streaming path. The plan explicitly flags its dedup changes as risk-sensitive ("keep the TTL-equivalent dedup in the reducer until Phase 4 lands; do not delete early") and calls for manual QA after Phases 3 and 5 — live streaming behavior that automated tests alone don't fully cover. It belongs in its own reviewed PR rather than bundled with the feature work.
Scope
Phase 3 — reducer-side merge/dedup consolidation
Move all merge logic from ChatRuntimeProvider into typed chatRuntimeSlice reducers so the provider shrinks to parse-and-dispatch (~300 lines), with no getState() and no full-array rebuilds.
One typed action per event family: toolEventReceived, subagentEventReceived, textDeltaReceived, turnLifecycleReceived. Migrate handler bodies (onToolCall/onToolResult at ChatRuntimeProvider.tsx:601/:665, the subagent_* handlers, onTextDelta, lifecycle) into reducers.
One dedup mechanism, reducer-level, on a unified row-id scheme ${threadId}:${requestId}:${kind}:${callId|taskId|streamId} — used identically by live handlers, hydrateRuntimeFromSnapshot, and hydrateRuntimeFromRunLedger. Kills the live-vs-ledger id divergence.
Replace preserveLiveSubagentProse (chatRuntimeSlice.ts) with an ordinary field-level upsert: persisted fields win for settled rows, live fields win for streaming fields.
Move the provider-local seenChatEventsRef (TTL map) and segmentDeliveriesRef into slice state (per-request segment state, cleared on chat_done).
Verbose debug logging on every dedup drop / merge decision (repo convention).
Deferred within Phase 3 / separate step: the backend seq envelope — progress_bridge.rs stamps a per-request monotonic seq: u64 + request_id on every event; reducers dedup by (requestId, seq); persist seq on PersistedToolTimelineEntry (turn_state/types.rs + app/src/types/turnState.ts). Until it lands, the TTL-map dedup moves verbatim into the reducer as the interim mechanism.
Phase 6 — cleanup
Delete the app/src/pages/Conversations.tsx re-export shim; point HumanPage/Accounts and the co-located test suites at features/conversations.
Delete dead state now subsumed: parallelStreamsByThread (if subsumed by streamingText items), preserveLiveSubagentProse, the provider refs removed in Phase 3.
Phase 1 remainder — shell/context extraction
Extract ThreadList, Composer/MicCloudComposer/useComposerState, and the page/sidebar shells out of features/conversations/Conversations.tsx, sharing thread/selection state via a ConversationsContext to avoid prop-drilling. All files ≤ ~500 lines.
Phase 3 reducers — merge/dedup logic lives in typed chatRuntimeSlice reducers; ChatRuntimeProvider is parse-and-dispatch only (no getState(), no full-array rebuilds).
Unified dedup + row ids — a single reducer-level dedup on ${threadId}:${requestId}:${kind}:${callId|taskId|streamId}, used identically by live handlers and both hydration paths; preserveLiveSubagentProse replaced by a field-level upsert.
seq envelope — progress_bridge.rs stamps (request_id, seq); reducers dedup by it and drop replayed seq <= lastSeq; seq persisted on PersistedToolTimelineEntry (Rust + TS).
Phase 6 cleanup — pages/Conversations.tsx shim deleted and consumers repointed; dead state removed.
Guardrail tests — ChatRuntimeProvider.test.tsx (assertions rewritten to dispatched actions), chatRuntimeSlice.test.ts + .toolFailure.test.ts, SubagentDrawer.test.tsx, plus new: duplicate-event replay is a no-op; live subagent prose survives snapshot hydration; reconnect-replay simulation.
Manual QA (per plan) — pnpm dev:app: multi-turn thread with tools + subagents; switch tab mid-stream and back; kill/restore socket mid-turn; proactive thread with no user message; hideAgentInsights sidebar variant in HumanPage; app restart on a 5-turn thread — all trails restored, ordered identically to live.
Diff coverage ≥ 80% — implementing PR(s) meet the changed-lines coverage gate (Vitest + cargo-llvm-cov, enforced by .github/workflows/ci-lite.yml).
Suggested sequencing: land Phase 3 (reducer consolidation with interim TTL dedup) → the seq envelope → Phase 6 cleanup → Phase 1 shell extraction. Each is an independently shippable, small PR.
Summary
Complete the remaining phases of the conversations timeline refactor that were deliberately deferred out of #4612 (which landed Phases 1–2 and 4–5 — the per-turn process history feature end-to-end). Three follow-up bodies of work remain, in priority order:
Design and rationale:
docs/plans/conversations-timeline-refactor.md(Phases 3, 6, and the Phase 1 shell-extraction items) anddocs/plans/per-turn-tool-timeline-history.md.Problem / Context
#4612 delivered the user-visible feature (each answer keeps its own tool-timeline trail) but intentionally stopped short of the internal-robustness refactor.
ChatRuntimeProvider.tsx(~1,664 lines) still merges ~28 snake_case socket events viastore.getState()+ find-row + full-array-replace, using two parallel dedup mechanisms with divergent row-id schemes, plus thepreserveLiveSubagentProsegraft (a documented live-only-state workaround). This is the "buggy in how it stores & streams" dimension from the audit.Why it was deferred: Phase 3 rewrites the live streaming path. The plan explicitly flags its dedup changes as risk-sensitive ("keep the TTL-equivalent dedup in the reducer until Phase 4 lands; do not delete early") and calls for manual QA after Phases 3 and 5 — live streaming behavior that automated tests alone don't fully cover. It belongs in its own reviewed PR rather than bundled with the feature work.
Scope
Phase 3 — reducer-side merge/dedup consolidation
Move all merge logic from
ChatRuntimeProviderinto typedchatRuntimeSlicereducers so the provider shrinks to parse-and-dispatch (~300 lines), with nogetState()and no full-array rebuilds.toolEventReceived,subagentEventReceived,textDeltaReceived,turnLifecycleReceived. Migrate handler bodies (onToolCall/onToolResultatChatRuntimeProvider.tsx:601/:665, thesubagent_*handlers,onTextDelta, lifecycle) into reducers.${threadId}:${requestId}:${kind}:${callId|taskId|streamId}— used identically by live handlers,hydrateRuntimeFromSnapshot, andhydrateRuntimeFromRunLedger. Kills the live-vs-ledger id divergence.preserveLiveSubagentProse(chatRuntimeSlice.ts) with an ordinary field-level upsert: persisted fields win for settled rows, live fields win for streaming fields.seenChatEventsRef(TTL map) andsegmentDeliveriesRefinto slice state (per-request segment state, cleared onchat_done).seqenvelope —progress_bridge.rsstamps a per-request monotonicseq: u64+request_idon every event; reducers dedup by(requestId, seq); persistseqonPersistedToolTimelineEntry(turn_state/types.rs+app/src/types/turnState.ts). Until it lands, the TTL-map dedup moves verbatim into the reducer as the interim mechanism.Phase 6 — cleanup
app/src/pages/Conversations.tsxre-export shim; pointHumanPage/Accountsand the co-located test suites atfeatures/conversations.parallelStreamsByThread(if subsumed bystreamingTextitems),preserveLiveSubagentProse, the provider refs removed in Phase 3.Phase 1 remainder — shell/context extraction
ThreadList,Composer/MicCloudComposer/useComposerState, and the page/sidebar shells out offeatures/conversations/Conversations.tsx, sharing thread/selection state via aConversationsContextto avoid prop-drilling. All files ≤ ~500 lines.Out of scope
Acceptance criteria
chatRuntimeSlicereducers;ChatRuntimeProvideris parse-and-dispatch only (nogetState(), no full-array rebuilds).${threadId}:${requestId}:${kind}:${callId|taskId|streamId}, used identically by live handlers and both hydration paths;preserveLiveSubagentProsereplaced by a field-level upsert.seqenvelope —progress_bridge.rsstamps(request_id, seq); reducers dedup by it and drop replayedseq <= lastSeq;seqpersisted onPersistedToolTimelineEntry(Rust + TS).pages/Conversations.tsxshim deleted and consumers repointed; dead state removed.ThreadList/Composer/MicCloudComposer/useComposerState/ page+sidebar shells extracted behindConversationsContext.ChatRuntimeProvider.test.tsx(assertions rewritten to dispatched actions),chatRuntimeSlice.test.ts+.toolFailure.test.ts,SubagentDrawer.test.tsx, plus new: duplicate-event replay is a no-op; live subagent prose survives snapshot hydration; reconnect-replay simulation.pnpm dev:app: multi-turn thread with tools + subagents; switch tab mid-stream and back; kill/restore socket mid-turn; proactive thread with no user message;hideAgentInsightssidebar variant in HumanPage; app restart on a 5-turn thread — all trails restored, ordered identically to live..github/workflows/ci-lite.yml).Related
docs/plans/conversations-timeline-refactor.md(Phases 3, 6, Phase 1 remainder),docs/plans/per-turn-tool-timeline-history.md.seqenvelope → Phase 6 cleanup → Phase 1 shell extraction. Each is an independently shippable, small PR.