Skip to content

Fix segmented chat response reconciliation#1469

Merged
senamakel merged 3 commits into
tinyhumansai:mainfrom
jwalin-shah:codex/SYM-158-chat-response-truncation
May 11, 2026
Merged

Fix segmented chat response reconciliation#1469
senamakel merged 3 commits into
tinyhumansai:mainfrom
jwalin-shah:codex/SYM-158-chat-response-truncation

Conversation

@jwalin-shah
Copy link
Copy Markdown
Contributor

@jwalin-shah jwalin-shah commented May 11, 2026

Summary

  • preserve segmented assistant responses without dropping later segments during reconciliation
  • add ChatRuntimeProvider coverage for multi-segment response behavior

Validation

  • pnpm --filter openhuman-app exec vitest run --config test/vitest.config.ts src/providers/tests/ChatRuntimeProvider.test.tsx
  • git diff --check upstream/main...HEAD

Notes

  • Base is upstream/main, not the stale fork origin/main. Against upstream/main this PR is limited to ChatRuntimeProvider and its tests.
  • The unrelated Operator MVP plan doc remains uncommitted and will be published separately.

Summary by CodeRabbit

  • Bug Fixes

    • Improved streaming chat delivery: more reliable reconstruction of streamed messages, prevention of duplicate appends, and handling of stale/missing segments so messages aren't lost or repeated.
  • Tests

    • Added and expanded tests covering segment reconciliation, duplicate suppression, ordering variations, and expiration scenarios to ensure robust streaming behavior.

Review Change Stack

@jwalin-shah jwalin-shah requested a review from a team May 11, 2026 05:31
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 11, 2026

📝 Walkthrough

Walkthrough

ChatRuntimeProvider adds a bounded, TTL-based per-turn segment-delivery cache and helpers (prune, get/create, take, delete). onSegment buffers segments, onDone takes and compares reconstructed concatenation exactly to event.full_response, and onError deletes delivery state. Tests expand reconciliation, ordering, dedupe, and TTL expiry cases.

Changes

Segment Delivery Reconciliation

Layer / File(s) Summary
Types and TTL config
app/src/providers/ChatRuntimeProvider.tsx
Adds segment-delivery cache TTL and max-entry limits; extends SegmentDelivery with createdAt and lastSeenAt; changes completeness to require exact reconstruction equality with event.full_response.
Cache lifecycle helpers
app/src/providers/ChatRuntimeProvider.tsx
Adds helpers to prune by TTL, get-or-create deliveries, take (read+remove) deliveries for onDone, and delete deliveries for onError/cleanup.
Event handler integrations
app/src/providers/ChatRuntimeProvider.tsx
Updates onSegment to record via get-or-create helper; onDone now takes delivery (read+delete) before completeness check; onError deletes delivery via helper.
Expanded tests / invariants
app/src/providers/__tests__/ChatRuntimeProvider.test.tsx
Adjusts duplicate chat_done dedupe test to await refetch; adds tests for reconstructing missing segments from chat_done.full_response, avoiding duplicate appends when segments already arrived, handling differing arrival order vs full_response, and TTL-expiration-driven re-append.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • tinyhumansai/openhuman#981: Both PRs modify ChatRuntimeProvider's onDone completion flow to queue snapshot refetch and centralize completion handling.
  • tinyhumansai/openhuman#1261: Both PRs modify ChatRuntimeProvider segment delivery tracking and reconciliation logic, including segmentDeliveriesRef, onSegment buffering, and related tests.

Suggested reviewers

  • graycyrus
  • senamakel

Poem

🐰 A rabbit pads through buffered streams,

Counting segments, timestamps, and dreams.
When pieces miss or orders stray,
Full response hops in to save the day.
TTL clears the trail—now reconcile away!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly describes the main change: fixing segmented chat response reconciliation, which aligns with the core objective of preserving segmented assistant responses without dropping later segments.
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.

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


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: 2

🤖 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/providers/ChatRuntimeProvider.tsx`:
- Around line 532-538: The segmentDeliveriesRef Map can grow unbounded if
terminal events (chat_done/chat_error) are missed; change creation/use of
entries (where deliveryKey is computed and segmentDeliveriesRef.current.get/set
is used) to a bounded/expiring cache: when creating a new delivery entry put an
object { segments: Map<number,string>, createdAt: number } and on every set/get
prune or replace entries older than a TTL (e.g. 5 minutes) and enforce a
maxEntries limit (LRU or simple oldest-first eviction) so stale turns are
removed automatically; apply the same bounded/expiring logic to all places that
call segmentDeliveriesRef.current.get/set (the blocks using segmentText,
segmentDeliveryKey, delivery.segments.set shown here and the other occurrences
referenced).
- Around line 64-77: The current hasCompleteSegmentDelivery incorrectly uses
event.full_response.includes(segment) which ignores order; instead, in
hasCompleteSegmentDelivery build a concatenated string by iterating indices
0..expected-1, retrieving delivery.segments.get(i) (from the
SegmentDelivery.segments Map), ensure none are undefined, append them in index
order, and finally compare the resulting concatenated string for exact equality
with event.full_response (and keep the existing checks for expected > 0 and
delivery.segments.size >= expected); update the function
hasCompleteSegmentDelivery to use this ordered concatenation equality check to
determine completeness.
🪄 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: a1f6c3eb-c3df-43c6-9524-384092703ea3

📥 Commits

Reviewing files that changed from the base of the PR and between 83bc564 and 227ef2c.

📒 Files selected for processing (2)
  • app/src/providers/ChatRuntimeProvider.tsx
  • app/src/providers/__tests__/ChatRuntimeProvider.test.tsx

Comment on lines +64 to +77
function hasCompleteSegmentDelivery(
event: ChatDoneEvent,
delivery: SegmentDelivery | undefined
): boolean {
const expected = event.segment_total ?? 0;
if (expected <= 0 || !delivery) return false;
if (delivery.segments.size < expected) return false;

for (let i = 0; i < expected; i += 1) {
const segment = delivery.segments.get(i);
if (segment === undefined || !event.full_response.includes(segment)) return false;
}
return true;
}
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Strengthen segment completeness check to preserve ordering correctness.

Current includes checks can mark delivery as “complete” even when segment order/content mapping is wrong, which can incorrectly skip reconciliation.

Proposed fix
 function hasCompleteSegmentDelivery(
   event: ChatDoneEvent,
   delivery: SegmentDelivery | undefined
 ): boolean {
   const expected = event.segment_total ?? 0;
   if (expected <= 0 || !delivery) return false;
   if (delivery.segments.size < expected) return false;
 
+  let cursor = 0;
   for (let i = 0; i < expected; i += 1) {
     const segment = delivery.segments.get(i);
-    if (segment === undefined || !event.full_response.includes(segment)) return false;
+    if (segment === undefined) return false;
+    const matchIndex = event.full_response.indexOf(segment, cursor);
+    if (matchIndex < 0) return false;
+    cursor = matchIndex + segment.length;
   }
   return true;
 }
🤖 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 64 - 77, The current
hasCompleteSegmentDelivery incorrectly uses
event.full_response.includes(segment) which ignores order; instead, in
hasCompleteSegmentDelivery build a concatenated string by iterating indices
0..expected-1, retrieving delivery.segments.get(i) (from the
SegmentDelivery.segments Map), ensure none are undefined, append them in index
order, and finally compare the resulting concatenated string for exact equality
with event.full_response (and keep the existing checks for expected > 0 and
delivery.segments.size >= expected); update the function
hasCompleteSegmentDelivery to use this ordered concatenation equality check to
determine completeness.

Comment on lines +532 to +538
const content = segmentText(event);
const deliveryKey = segmentDeliveryKey(event.thread_id, event.request_id);
const delivery = segmentDeliveriesRef.current.get(deliveryKey) ?? {
segments: new Map<number, string>(),
};
delivery.segments.set(event.segment_index, content);
segmentDeliveriesRef.current.set(deliveryKey, delivery);
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Bound segmentDeliveriesRef to avoid stale-turn memory growth.

Entries are only removed on chat_done / chat_error. If terminal events are missed, this cache can accumulate for the lifetime of the provider.

Proposed fix
-type SegmentDelivery = { segments: Map<number, string> };
+type SegmentDelivery = { segments: Map<number, string>; updatedAt: number };

 // in onSegment
+const now = Date.now();
 const delivery = segmentDeliveriesRef.current.get(deliveryKey) ?? {
   segments: new Map<number, string>(),
+  updatedAt: now,
 };
 delivery.segments.set(event.segment_index, content);
+delivery.updatedAt = now;
 segmentDeliveriesRef.current.set(deliveryKey, delivery);

+// prune stale deliveries
+const ttlMs = 10 * 60_000;
+for (const [key, value] of segmentDeliveriesRef.current) {
+  if (now - value.updatedAt > ttlMs) {
+    segmentDeliveriesRef.current.delete(key);
+  }
+}

Also applies to: 662-666, 767-767

🤖 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 532 - 538, The
segmentDeliveriesRef Map can grow unbounded if terminal events
(chat_done/chat_error) are missed; change creation/use of entries (where
deliveryKey is computed and segmentDeliveriesRef.current.get/set is used) to a
bounded/expiring cache: when creating a new delivery entry put an object {
segments: Map<number,string>, createdAt: number } and on every set/get prune or
replace entries older than a TTL (e.g. 5 minutes) and enforce a maxEntries limit
(LRU or simple oldest-first eviction) so stale turns are removed automatically;
apply the same bounded/expiring logic to all places that call
segmentDeliveriesRef.current.get/set (the blocks using segmentText,
segmentDeliveryKey, delivery.segments.set shown here and the other occurrences
referenced).

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.

🧹 Nitpick comments (2)
app/src/providers/__tests__/ChatRuntimeProvider.test.tsx (2)

327-368: 💤 Low value

Good coverage for the strict-equality reconciliation branch.

This test exercises exactly the case where includes-style completeness checks used to mis-classify the turn as complete: the buffered concatenation ('Alpha' + 'Beta') doesn't equal event.full_response ('BetaAlpha'), so reconciliation correctly appends the canonical response and we end at 3 total appends.

A tiny note on the test name: "not in full_response order" reads as if the segments themselves arrived out of order, but the scenario is really that the server's full_response orders content differently than segment_index. Renaming to something like "reconciles when buffered segments don't reconstruct full_response" would describe the assertion more precisely. Non-blocking.

🤖 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/__tests__/ChatRuntimeProvider.test.tsx` around lines 327 -
368, The test name is slightly misleading; rename the test description string in
the test case for clarity—change the it(...) label currently "reconciles when
segments are present but not in full_response order" to a clearer description
such as "reconciles when buffered segments don't reconstruct full_response" so
it accurately reflects that the server full_response ordering differs from
segment_index rather than out-of-order arrivals (update the test case
identification string inside the it(...) call in ChatRuntimeProvider.test.tsx).

370-404: 💤 Low value

TTL expiry test is solid; consider also asserting the appended content.

The boundary offset (SEGMENT_DELIVERY_TTL_MS + 1) and try/finally around nowSpy.mockRestore() correctly drive the stale-eviction path through takeSegmentDelivery → prune → undefined → reconciliation, and you verify the second append fires after expiry.

Optional: in addition to toHaveBeenCalledTimes(2), asserting the second call's content (e.g. expect.objectContaining({ content: 'Stale segment.', sender: 'agent' })) would lock in that the reconciliation append carries the full_response payload rather than e.g. an empty/error string, in line with the behavior-over-implementation guideline.

Proposed addition
         await waitFor(() => expect(threadApi.appendMessage).toHaveBeenCalledTimes(2));
+        expect(threadApi.appendMessage).toHaveBeenLastCalledWith(
+          't-stale',
+          expect.objectContaining({ content: 'Stale segment.', sender: 'agent' })
+        );

As per coding guidelines: "Prefer behavior over implementation in tests".

🤖 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/__tests__/ChatRuntimeProvider.test.tsx` around lines 370 -
404, Add an assertion that the reconciliation append actually carries the stale
payload: after the second waitFor that checks threadApi.appendMessage call
count, assert the second call (threadApi.appendMessage call `#2` / index 1) was
invoked with an argument matching the expected message shape (object containing
content: 'Stale segment.' and sender: 'agent' or whatever sender your code
uses). Locate this in the test around listeners.onSegment / listeners.onDone and
use the existing threadApi.appendMessage mock to assert the second call's
payload (e.g., via expect.objectContaining or checking mock.calls[1][0]) so the
test verifies behavior not just call count; references: listeners.onSegment,
listeners.onDone, threadApi.appendMessage, SEGMENT_DELIVERY_TTL_MS /
takeSegmentDelivery.
🤖 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.

Nitpick comments:
In `@app/src/providers/__tests__/ChatRuntimeProvider.test.tsx`:
- Around line 327-368: The test name is slightly misleading; rename the test
description string in the test case for clarity—change the it(...) label
currently "reconciles when segments are present but not in full_response order"
to a clearer description such as "reconciles when buffered segments don't
reconstruct full_response" so it accurately reflects that the server
full_response ordering differs from segment_index rather than out-of-order
arrivals (update the test case identification string inside the it(...) call in
ChatRuntimeProvider.test.tsx).
- Around line 370-404: Add an assertion that the reconciliation append actually
carries the stale payload: after the second waitFor that checks
threadApi.appendMessage call count, assert the second call
(threadApi.appendMessage call `#2` / index 1) was invoked with an argument
matching the expected message shape (object containing content: 'Stale segment.'
and sender: 'agent' or whatever sender your code uses). Locate this in the test
around listeners.onSegment / listeners.onDone and use the existing
threadApi.appendMessage mock to assert the second call's payload (e.g., via
expect.objectContaining or checking mock.calls[1][0]) so the test verifies
behavior not just call count; references: listeners.onSegment, listeners.onDone,
threadApi.appendMessage, SEGMENT_DELIVERY_TTL_MS / takeSegmentDelivery.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 88ca465b-3815-455b-9560-fdae2c082269

📥 Commits

Reviewing files that changed from the base of the PR and between 227ef2c and 8e54f30.

📒 Files selected for processing (2)
  • app/src/providers/ChatRuntimeProvider.tsx
  • app/src/providers/__tests__/ChatRuntimeProvider.test.tsx

@senamakel senamakel merged commit 87ea53c into tinyhumansai:main May 11, 2026
20 checks passed
AusAgentSmith pushed a commit to AusAgentSmith/openhuman that referenced this pull request May 23, 2026
Co-authored-by: Jwalin Shah <jshah1331@gmail.com>
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.

3 participants