Skip to content

fix(chat): keep inference alive across tab switches#583

Closed
M3gA-Mind wants to merge 3 commits intotinyhumansai:mainfrom
M3gA-Mind:feat/chat-thinking-upgrade
Closed

fix(chat): keep inference alive across tab switches#583
M3gA-Mind wants to merge 3 commits intotinyhumansai:mainfrom
M3gA-Mind:feat/chat-thinking-upgrade

Conversation

@M3gA-Mind
Copy link
Copy Markdown
Contributor

@M3gA-Mind M3gA-Mind commented Apr 15, 2026

Summary

  • move chat socket event handling out of Conversations into a global chatEventManager so in-flight agent runs continue across route/tab switches
  • add inferenceSlice runtime Redux state for per-thread sending, streaming, inference status, and tool timeline so remounting chat rehydrates correctly
  • add regression coverage for remount recovery plus new E2E tab-switch flow and Mac2 selector escaping fixes

Test plan

  • yarn workspace openhuman-app compile
  • yarn workspace openhuman-app lint
  • yarn workspace openhuman-app format:check
  • yarn workspace openhuman-app build
  • yarn vitest run src/pages/__tests__/Conversations.remount.test.tsx src/store/__tests__/inferenceSlice.test.ts src/services/__tests__/chatEventManager.test.ts
  • bash app/scripts/e2e-run-spec.sh test/e2e/specs/chat-tab-switch.spec.ts chat-tab-switch (currently flaky on macOS Appium session/auth state)

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Chat messages now persist and recover when navigating between pages or tabs during message transmission.
    • Added E2E test coverage for in-flight chat message recovery during tab switching.
  • Bug Fixes

    • Fixed E2E test XPath selectors to properly handle special characters.
  • Tests

    • Added comprehensive test coverage for chat event handling and per-thread message state management.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 15, 2026

Warning

Rate limit exceeded

@M3gA-Mind has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 36 minutes and 17 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 36 minutes and 17 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4178e13a-f660-492f-95ae-c41202fb53a9

📥 Commits

Reviewing files that changed from the base of the PR and between b7fbe31 and 7a03bac.

📒 Files selected for processing (5)
  • app/src/pages/Conversations.tsx
  • app/src/providers/SocketProvider.tsx
  • app/src/services/__tests__/chatEventManager.test.ts
  • app/src/services/chatEventManager.ts
  • app/test/e2e/specs/chat-tab-switch.spec.ts
📝 Walkthrough

Walkthrough

This PR centralizes chat event listeners from Conversations.tsx into a new chatEventManager singleton that subscribes to socket events and updates Redux state. SocketProvider now manages the manager's lifecycle alongside socket connection. A new Redux slice (inferenceSlice) persists per-thread inference state (sending status, tool timeline, streaming data). E2E test infrastructure improvements include XML escaping for XPath selectors and a new chat tab switch recovery test.

Changes

Cohort / File(s) Summary
Redux Inference State
app/src/store/inferenceSlice.ts, app/src/store/index.ts
New Redux slice managing per-thread runtime state: sendingByThread, inferenceStatusByThread, toolTimelineByThread, streamingAssistantByThread with reducers to set/clear/upsert values. Integrated into store configuration.
Centralized Chat Event Management
app/src/services/chatEventManager.ts, app/src/providers/SocketProvider.tsx
New chatEventManager service handles real-time chat lifecycle events (segment, tool calls, inference, done, error) with TTL-based deduplication and pending reaction tracking. SocketProvider initializes/tears down manager on socket connect/disconnect.
Conversations Component Refactoring
app/src/pages/Conversations.tsx
Removed direct event subscription and local inference state; now reads sendingByThread, inferenceStatusByThread, toolTimelineByThread, streamingAssistantByThread from Redux and dispatches state updates via chatEventManager integration. Simplified to pure consumer of Redux state.
Unit Tests
app/src/store/__tests__/inferenceSlice.test.ts, app/src/services/__tests__/chatEventManager.test.ts, app/src/pages/__tests__/Conversations.remount.test.tsx
New test suites validating Redux slice reducers, chatEventManager initialization/event handling and deduplication, and Conversations remount recovery behavior.
E2E Testing Infrastructure
app/test/e2e/helpers/element-helpers.ts, app/test/e2e/specs/chat-tab-switch.spec.ts
Enhanced XPath helpers with XML character escaping for safer text selectors. New E2E spec for in-flight chat recovery when switching tabs, including fallback input mechanisms.
Project Documentation
.claude/memory.md
Added memory sections documenting architectural change (Issue #577) and E2E/Mac2 testing constraints (disallow browser.execute on Appium, XML-escaping requirement).

Sequence Diagram

sequenceDiagram
    participant SocketProvider
    participant ChatEventManager as ChatEventManager<br/>(Singleton)
    participant ChatService
    participant Redux as Redux Store
    participant Conversations

    Note over SocketProvider,Conversations: Socket Connection Lifecycle
    SocketProvider->>ChatEventManager: init() on sessionToken change
    ChatEventManager->>ChatService: subscribeChatEvents(listeners)
    ChatService-->>ChatEventManager: return unsubscribe cleanup

    Note over SocketProvider,Conversations: Real-time Event Handling
    ChatService->>ChatEventManager: onSegment(event)
    ChatEventManager->>ChatEventManager: Check deduplication (seenChatEvents)
    ChatEventManager->>Redux: dispatch(setToolTimelineForThread)
    ChatEventManager->>Redux: dispatch(upsertStreamingForThread)
    
    ChatService->>ChatEventManager: onDone(event)
    ChatEventManager->>Redux: dispatch(clearInferenceStatusForThread)
    ChatEventManager->>Redux: dispatch(clearStreamingForThread)
    ChatEventManager->>Redux: dispatch(setActiveThread(null))

    Note over SocketProvider,Conversations: Component Reads Redux State
    Redux-->>Conversations: inferenceState.sendingByThread[threadId]
    Redux-->>Conversations: inferenceState.toolTimelineByThread[threadId]
    Redux-->>Conversations: inferenceState.streamingAssistantByThread[threadId]

    Note over SocketProvider,Conversations: Disconnect Lifecycle
    SocketProvider->>ChatEventManager: teardown() on sessionToken → null
    ChatEventManager->>ChatService: call unsubscribe cleanup
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • senamakel

Poem

🐰 Hops excitedly
Events once scattered, now gather in one place,
Redux holds memories of each threaded race,
Socket lifecycle dances with care,
Chat flows pure through the Redux stair,
State persists through remount's repair! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(chat): keep inference alive across tab switches' directly reflects the main objective: extracting chat event handling to preserve in-flight agent runs during route/tab navigation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
app/test/e2e/specs/chat-tab-switch.spec.ts (1)

101-109: Add diagnostics around request/response wait failures.

If Line 101 or Line 108 fails, triage is still expensive. Capture request-log snapshots and accessibility tree in those failure paths too (not only when input is missing).

🛠️ Suggested refactor
     const chatReq = await waitForRequest('POST', '/openai/v1/chat/completions', 30_000);
+    if (!chatReq) {
+      stepLog('chat completion request not observed', getRequestLog().slice(-20));
+      const tree = await dumpAccessibilityTree();
+      stepLog('accessibility snapshot on missing request', tree.slice(0, 4000));
+    }
     expect(chatReq).toBeDefined();
@@
     await navigateToConversations();
-    await waitForText('Hello from e2e mock agent', 30_000);
+    try {
+      await waitForText('Hello from e2e mock agent', 30_000);
+    } catch (error) {
+      stepLog('assistant response not visible after tab return', getRequestLog().slice(-20));
+      const tree = await dumpAccessibilityTree();
+      stepLog('accessibility snapshot on missing response', tree.slice(0, 4000));
+      throw error;
+    }
     expect(await textExists('Type a message...')).toBe(true);

As per coding guidelines: "Add failure diagnostics (request logs, dumpAccessibilityTree()) for faster debugging by agents in E2E specs."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/test/e2e/specs/chat-tab-switch.spec.ts` around lines 101 - 109, Wrap the
critical waits (waitForRequest('POST', '/openai/v1/chat/completions'),
waitForText('Hello from e2e mock agent'), and the textExists('Type a
message...') assertion) in try/catch (or add .catch handlers) and on any failure
capture diagnostics: save request logs/ snapshots for the failing endpoint and
call dumpAccessibilityTree() before rethrowing; include context (which wait
failed and relevant request/response payloads) so failures in waitForRequest,
waitForText, or textExists produce request-log snapshots and an accessibility
tree for faster triage.
app/src/services/chatEventManager.ts (1)

77-428: Add event-level debug logs around the new recovery path.

The manager logs init/teardown, but not which events were applied, deduped, or used to clear thread state. That makes tab-switch recovery regressions hard to trace when a thread gets stuck or an event is dropped.

As per coding guidelines: "Add substantial, development-oriented logs ... log critical checkpoints including entry/exit points, branch decisions, external calls, retries/timeouts, state transitions, and error handling paths."

Also applies to: 431-455

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/services/chatEventManager.ts` around lines 77 - 428, Add
development-level debug logs at the start and at key branch points inside each
event handler to trace which events are applied, deduped, skipped, or cause
state clears; specifically, instrument setInferenceStart, setIterationStart,
setToolCall, setToolResult, setSubagentSpawned, setSubagentDone, setSegment,
setTextDelta, setThinkingDelta, setToolArgsDelta, setDone, and setError to log
the incoming event (thread_id, request_id, round, tool_name, tool_call_id,
success/error_type), the result of markChatEventSeen checks, decisions taken
(e.g., existingIdx found, merged/added entry, changed=false/true), and
state-clear actions
(clearInferenceStatusForThread/clearStreamingForThread/setActiveThread); use the
project's debug logger (or console.debug if none) and keep messages concise and
consistent so developers can trace entry/exit and branch outcomes for the
recovery path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/pages/Conversations.tsx`:
- Around line 427-439: The timeout/failure handler only resets sending and
activeThread (sendingTimeoutRef, dispatch(setThreadSending...), setSendError,
chatEventManager.clearPendingReaction, dispatch(setActiveThread(null))) but
leaves per-thread runtime state like inferenceStatusByThread,
streamingAssistantByThread and any running tool entries, which causes stale
“Thinking…” UI; update these failure branches (the timeout block and the similar
branch at 453-461) to also clear/remove the thread's entries from
inferenceStatusByThread and streamingAssistantByThread and to cancel/clear any
running tools for sendingThreadId (via your chat/tool manager), by dispatching
the appropriate Redux actions or calling the existing chatEventManager/tool
cleanup methods for that thread so all per-thread runtime state is fully
cleared.
- Around line 321-324: The send-watchdog timeout is currently stored in the
component-scoped sendingTimeoutRef and cleared on unmount, which loses the
fallback that flips sendingByThread back to false; instead move the timeout
storage out of the component lifecycle (e.g., a module-level Map or WeakMap
keyed by threadId) so timeouts survive route/tab switches, set the timeout when
marking a thread as sending, and only clear that module-level timeout when a
chat_done or chat_error handler runs or when the timeout fires to reset
sendingByThread; do not clear the module-level timeout in the Conversations
component cleanup—update references from sendingTimeoutRef to the new
module-level storage and adjust the chat_done/chat_error handlers to clear the
corresponding entry.

In `@app/src/services/__tests__/chatEventManager.test.ts`:
- Around line 47-69: This test initializes the module-scoped singleton
chatEventManager but never tears it down, risking cross-test state leakage; add
a teardown at the end of this test that calls a cleanup method on the singleton
(e.g., chatEventManager.destroy() or chatEventManager.teardown()) to unsubscribe
its listeners and reset internal state, and if such a method doesn't exist add a
test-only cleanup method on chatEventManager to remove subscriptions (matching
how mockSubscribeChatEvents registers listeners) so mockSubscribeChatEvents and
setActiveThread interactions are isolated between tests.

In `@app/src/services/chatEventManager.ts`:
- Around line 228-249: The handler setSubagentDone currently updates every
timeline entry whose name equals `🤖 ${event.tool_name}` and status ===
'running'; change it to update only the single matching spawned entry
(preferably by stable ID if the entries have one, e.g., entry.id or
entry.stableId, otherwise pick the latest running entry). In setSubagentDone
locate the target entry index in
store.getState().inference.toolTimelineByThread[event.thread_id] by matching
entry.id === event.subagent_id (or fallback: find last index where entry.name
=== `🤖 ${event.tool_name}` && entry.status === 'running'), then call
setToolTimelineForThread with entries mapped to update only that index (leave
all others unchanged). Keep the inference status update
(setInferenceStatusForThread) as-is after this change.

---

Nitpick comments:
In `@app/src/services/chatEventManager.ts`:
- Around line 77-428: Add development-level debug logs at the start and at key
branch points inside each event handler to trace which events are applied,
deduped, skipped, or cause state clears; specifically, instrument
setInferenceStart, setIterationStart, setToolCall, setToolResult,
setSubagentSpawned, setSubagentDone, setSegment, setTextDelta, setThinkingDelta,
setToolArgsDelta, setDone, and setError to log the incoming event (thread_id,
request_id, round, tool_name, tool_call_id, success/error_type), the result of
markChatEventSeen checks, decisions taken (e.g., existingIdx found, merged/added
entry, changed=false/true), and state-clear actions
(clearInferenceStatusForThread/clearStreamingForThread/setActiveThread); use the
project's debug logger (or console.debug if none) and keep messages concise and
consistent so developers can trace entry/exit and branch outcomes for the
recovery path.

In `@app/test/e2e/specs/chat-tab-switch.spec.ts`:
- Around line 101-109: Wrap the critical waits (waitForRequest('POST',
'/openai/v1/chat/completions'), waitForText('Hello from e2e mock agent'), and
the textExists('Type a message...') assertion) in try/catch (or add .catch
handlers) and on any failure capture diagnostics: save request logs/ snapshots
for the failing endpoint and call dumpAccessibilityTree() before rethrowing;
include context (which wait failed and relevant request/response payloads) so
failures in waitForRequest, waitForText, or textExists produce request-log
snapshots and an accessibility tree for faster triage.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 26b20ae4-7954-412d-9548-3f3e86b3e1ab

📥 Commits

Reviewing files that changed from the base of the PR and between a2aee3a and b7fbe31.

📒 Files selected for processing (11)
  • .claude/memory.md
  • app/src/pages/Conversations.tsx
  • app/src/pages/__tests__/Conversations.remount.test.tsx
  • app/src/providers/SocketProvider.tsx
  • app/src/services/__tests__/chatEventManager.test.ts
  • app/src/services/chatEventManager.ts
  • app/src/store/__tests__/inferenceSlice.test.ts
  • app/src/store/index.ts
  • app/src/store/inferenceSlice.ts
  • app/test/e2e/helpers/element-helpers.ts
  • app/test/e2e/specs/chat-tab-switch.spec.ts

Comment thread app/src/pages/Conversations.tsx Outdated
Comment thread app/src/pages/Conversations.tsx Outdated
Comment thread app/src/services/__tests__/chatEventManager.test.ts
Comment thread app/src/services/chatEventManager.ts
Address CodeRabbit feedback for watchdog lifecycle, runtime cleanup,
subagent timeline matching, and E2E diagnostics.

Made-with: Cursor
@M3gA-Mind M3gA-Mind closed this Apr 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant