Skip to content

Restore server-side reasoning content projection (pending upstream TanStack DB fix)#4532

Merged
kevin-dp merged 2 commits into
kevin/reasoning-contentfrom
kevin/reasoning-content-server-projection
Jun 9, 2026
Merged

Restore server-side reasoning content projection (pending upstream TanStack DB fix)#4532
kevin-dp merged 2 commits into
kevin/reasoning-contentfrom
kevin/reasoning-content-server-projection

Conversation

@kevin-dp

@kevin-dp kevin-dp commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #4508. Restores the proper server-side projection for
reasoning content — i.e. run.reasoning.content computed via
concat(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:

  • `@durable-streams/state` sync layer applies events differently
    than direct `.insert()` / `.update()` calls.
  • React-DOM scheduler interactions with `useLiveQuery` under
    transitions / suspense.
  • HMR-accumulated live-query state (the production session showed
    `live-query-15` and later `live-query-33` against the same
    underlying collections).

What this PR changes vs. #4508

  • `entity-timeline.ts` — add `content: concat(toArray())`
    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.
  • `EntityTimelineReasoningItem` — reinstates `content: string`.
  • `EntityTimelineReasoningDeltaItem` — removed.
  • `client.ts` — drops the now-unused export.
  • `AgentResponseLive` — drops the `run.reasoningDeltas`
    subscription and the client-side concat. `reasoningEntries`
    reads `content` straight off the projected row.
  • Tests — three reasoning-content tests assert
    `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

  1. Upstream TanStack DB ships a fix for the long-lived
    `concat(toArray)` correlated sub-query staleness.
  2. The reasoning-content tests in `entity-timeline.test.ts` pass
    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

  • TanStack DB upstream fix lands and feat(agents-server-ui): stream model reasoning into the UI #4508 is rebased onto a
    bumped `@tanstack/db` peer.
  • `pnpm -C packages/agents-runtime test` — entity-timeline tests
    still pass.
  • Manual: send a Claude prompt with extended thinking, wait for
    reasoning + answer to finish, click "Thought for Ns" → full
    content renders without needing a renderer reload.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Electric Agents Desktop Builds

Build artifacts for commit 2c72060.

Platform Status Artifact
macOS Apple Silicon Passed DMG
macOS Intel Passed DMG
Windows x64 Passed Installer
Linux x64 Passed AppImage / deb

Workflow run

@codecov

codecov Bot commented Jun 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 38.87%. Comparing base (7d8ef81) to head (2c72060).
⚠️ Report is 2 commits behind head on kevin/reasoning-content.

Files with missing lines Patch % Lines
.../agents-server-ui/src/components/AgentResponse.tsx 0.00% 6 Missing ⚠️
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     
Flag Coverage Δ
packages/agents 74.84% <ø> (+3.69%) ⬆️
packages/agents-mcp 77.54% <ø> (ø)
packages/agents-mobile 66.92% <ø> (ø)
packages/agents-server 74.19% <ø> (+0.24%) ⬆️
packages/agents-server-ui 5.41% <0.00%> (-0.77%) ⬇️
packages/electric-ax 46.42% <ø> (ø)
packages/experimental 87.73% <ø> (ø)
packages/react-hooks 86.48% <ø> (ø)
packages/start 82.83% <ø> (ø)
packages/typescript-client 91.83% <ø> (+0.11%) ⬆️
packages/y-electric 56.05% <ø> (ø)
typescript 38.50% <0.00%> (-2.46%) ⬇️
unit-tests 38.50% <0.00%> (-2.46%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Electric Agents Mobile Build

Local mobile checks ran for commit 2c72060.

The EAS Android preview build was skipped because the mobile-eas-build label is not present.
Add the mobile-eas-build label to this PR to produce an installable preview build.

Workflow run

…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>
@kevin-dp kevin-dp force-pushed the kevin/reasoning-content-server-projection branch 2 times, most recently from b879f1b to 4c5d6fb Compare June 9, 2026 08:30
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>
@kevin-dp kevin-dp merged commit 2c72060 into kevin/reasoning-content Jun 9, 2026
32 of 35 checks passed
@kevin-dp kevin-dp deleted the kevin/reasoning-content-server-projection branch June 9, 2026 09:52
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