Restore server-side reasoning content projection (pending upstream TanStack DB fix)#4532
Merged
kevin-dp merged 2 commits intoJun 9, 2026
Conversation
Contributor
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## kevin/reasoning-content #4532 +/- ##
===========================================================
- Coverage 40.96% 38.87% -2.09%
===========================================================
Files 287 288 +1
Lines 23668 26163 +2495
Branches 7887 9079 +1192
===========================================================
+ Hits 9696 10172 +476
- Misses 13900 15913 +2013
- Partials 72 78 +6
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Contributor
Electric Agents Mobile BuildLocal mobile checks ran for commit The EAS Android preview build was skipped because the |
…ection Reverts the client-side `run.reasoningDeltas` workaround in favor of the server-side `concat(toArray(...))` projection on `run.reasoning.content`. Currently broken in production against `@tanstack/db@0.6.7` — documented in `packages/agents-runtime/test/entity-timeline.test.ts`'s `reasoning content remains populated after status flips to completed` and friends. Unit tests against the projection pass cleanly; the bug only surfaces in a long-lived stream-backed live query after the parent row's `.update()`, with the field silently becoming `null` even though deltas are present in the local DB. A fresh subscription (navigate-away + back, or reload) recovers. Holding this branch as a draft PR so the work isn't lost. Merge once TanStack DB ships an upstream fix that makes the placeholder tests pass against a long-lived production live query. Diff vs `kevin/reasoning-content`: - `entity-timeline.ts` — add `content: concat(toArray(<delta-join>))` back to `reasoning.select(...)`, drop the parallel `reasoningDeltas` sub-collection. Alias stays `reasoningChunk` (not the generic `chunk`) to avoid the alias-collision class of bug. - `EntityTimelineReasoningItem` — `content: string` reinstated; `EntityTimelineReasoningDeltaItem` removed. - `client.ts` — drop `EntityTimelineReasoningDeltaItem` export. - `AgentResponseLive` — drop the `run.reasoningDeltas` subscription + client-side concat; `reasoningEntries` reads `content` straight off the projected row. - Tests — three reasoning-content tests assert `reasoning[0].content` (rather than concatenating raw deltas). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
b879f1b to
4c5d6fb
Compare
Tracks down and fixes the bug that's been driving the client-side-concat workaround in #4508 and blocking #4532. ## Root cause TanStack DB's "includes" — fields whose value is a sub-query like \`concat(toArray(...))\` — are deferred. A row carrying an include arrives with the field set to \`null\` and a hidden \`Symbol(includesRouting)\` marker describing how to compute it. The include is only materialized when something downstream reads it *in the right way*. The empirical rule (figured out via DevTools probes — \`.toArray\` on the sub-collection always showed the populated string, \`useLiveQuery\` output had \`content: null\`): **An include is materialized only when it's referenced inside a \`caseWhen\` object body in a downstream \`.select(...)\`. A bare top-level reference doesn't trigger it — the include is just aliased forward, still deferred.** This is why \`items.text.content\` has always worked and reasoning hasn't. The items consumer derefs \`item.textContent\` inside the \`text: caseWhen(item.text.key, { ..., content: item.textContent })\` body. The reasoning consumer had \`content: concat(toArray(...))\` (or, after the source/consumer split, \`content: r.reasoningContent\`) at the top level of its select. useLiveQuery handed the row to React with \`content: null\`. ## Fix Wrap the include reference inside a \`caseWhen\` object body, mirroring items: \`\`\`ts reasoning: q .from({ r: runReasoningSource }) ... .select(({ r }) => ({ key: r.key, run_id: r.run_id, order: r.order, status: r.status, body: caseWhen(r.key, { content: r.reasoningContent, }), summary_title: r.summary_title, encrypted: r.encrypted, })) \`\`\` \`r.key\` is always truthy on a real row, so the caseWhen is effectively unconditional — its only purpose is being an object body that forces the include reference to materialize. UI reads \`entry.body?.content\` (via the type) and \`AgentResponseLive\` maps it back into a flat \`content: string\` on \`ReasoningEntry\` so \`ReasoningSection\`'s API is unchanged. This drops the need for the client-side concat workaround that was the original target of #4532. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to #4508. Restores the proper server-side projection for
reasoning content — i.e.
run.reasoning.contentcomputed viaconcat(toArray(<delta-join>))directly in the timeline live query— and removes the client-side concatenation workaround that ships in
the parent PR.
Currently broken in production against `@tanstack/db@0.6.7`.
Holding as a draft so the work isn't lost. Do not merge until
the upstream TanStack DB issue is fixed (see below).
Why this PR exists
In #4508 we shipped client-side delta concatenation because the
server-side projection silently went stale in the running app:
`run.reasoning[i].content` came back as `null` once the row's
status flipped to `completed`, even though the deltas were still
present in `db.collections.reasoningDeltas`. A fresh subscription
(reload, navigate-away-and-back) always recovered. Unit tests of the
same projection shape pass cleanly — the bug only reproduces in the
running stack with a long-lived stream-fed live query.
I tried seven reproduction variants against TanStack DB's
`localOnlyCollectionOptions` (basic `.update()`, deltas-after-
update, concurrent update+sibling-insert, `_seq` bump, second live
query mid-flight, immediate-after-update polling, stringly-typed
order). All passed. Suspected differences vs. production:
than direct `.insert()` / `.update()` calls.
transitions / suspense.
`live-query-15` and later `live-query-33` against the same
underlying collections).
What this PR changes vs. #4508
back to `reasoning.select(...)`. Drop the parallel
`reasoningDeltas` sub-collection on `EntityTimelineRunRow`.
Alias stays `reasoningChunk` (not `chunk`) to avoid the
alias-collision class of bug we hit and fixed for text-content
in the parent PR.
subscription and the client-side concat. `reasoningEntries`
reads `content` straight off the projected row.
`reasoning[0].content` directly (rather than concatenating raw
deltas client-side).
All 60 unit tests still pass — the bug doesn't surface in tests.
Merge criteria
`concat(toArray)` correlated sub-query staleness.
against a real Electron renderer session — i.e. exercise the
placeholder "Thought for Ns" expand path on `kevin/reasoning-content-server-projection`
without seeing `content: null`.
Until then this PR stays a draft and #4508 ships the client-side
workaround.
Test plan
bumped `@tanstack/db` peer.
still pass.
reasoning + answer to finish, click "Thought for Ns" → full
content renders without needing a renderer reload.
🤖 Generated with Claude Code