Skip to content

[Bug]: Assistant streaming causes full-thread projection scans for every text delta #5719

Description

@cheruvian

Steps to reproduce

  1. Run T3 Code 0.0.32 with a mature thread containing substantial message and activity history.
  2. Enable Settings → Assistant output → Stream assistant messages.
  3. Start a Codex turn that streams a moderately long response.
  4. Observe CPU and RSS for the T3 server process (not only the provider subprocess).
  5. Compare with assistant streaming disabled.

The degradation grows with both the number of streamed response chunks and the amount of existing thread history.

Expected behavior

Streaming an assistant response should have approximately constant per-delta projection cost. Assistant text deltas should not repeatedly load and decode complete message, proposed-plan, activity, and approval collections.

Actual behavior

Each provider assistant-text delta is dispatched as a durable thread.message.assistant.delta command and becomes a thread.message-sent event.

Every event is then applied serially to all nine projectors. The threads projector handles every thread.message-sent event by invoking refreshThreadShellSummary, which reloads all messages, proposed plans, activities, and pending approvals for that thread.

Sanitized measurements from a real 0.0.32 database:

  • 3,610 logical messages produced 128,972 thread.message-sent events
  • Average: 35.7 persisted chunk events per logical message
  • Maximum observed: 2,436 events for one 11,646-character message
  • Busiest thread: 25,878 orchestration events
  • With approximately one new orchestration event per second, the T3 server sustained 110–165% CPU
  • Server RSS oscillated between approximately 1.4 GB and 2.6 GB

The observed cost therefore scales with streamed chunks × accumulated thread history. Repeatedly loading and decoding the full activity collection also creates significant allocation and garbage-collection pressure.

Relevant source paths

  • apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts: when enableAssistantStreaming is true, each assistant delta is individually dispatched (currently around lines 1638–1683).
  • apps/server/src/orchestration/Layers/ProjectionPipeline.ts: refreshThreadShellSummary loads all four thread collections (currently around lines 547–589).
  • The same projector calls that refresh for every thread.message-sent event (currently around lines 845–860).
  • Every event runs nine projectors serially, with a transactional projector cursor update for each one (currently around lines 1601–1692).

This appears to undermine the intent of #1647, which removed an earlier thread-wide scan from message projection.

Issue #2761 is adjacent but appears distinct: that report concerns large WebSocket snapshots and reconnect behavior, while this report concerns synchronous persistence/projection work during active streaming.

Suggested direction

The narrowest fix may be to avoid refreshThreadShellSummary for assistant thread.message-sent events. Assistant text cannot change latestUserMessageAt, pending approvals, pending user input, or actionable proposed-plan state.

More generally, shell-summary fields could be maintained incrementally or through targeted aggregate queries for the event types that can actually affect each field. A useful regression contract would be: projecting an assistant delta performs a constant number of queries independent of thread history and does not query activities, proposed plans, or approvals.

Impact

Major degradation or frequent failure.

Version or commit

T3 Code Alpha 0.0.32.

Environment

macOS on Apple Silicon, desktop app, Codex app-server provider, assistant streaming enabled.

Logs or stack traces

No raw logs or database artifacts are attached because they can contain private agent content. The measurements above contain aggregate counts only.

Workaround

Disable Stream assistant messages. The server checks this setting while processing deltas, so subsequent text is handled by the existing buffered path without terminating the provider session. Starting a fresh thread reduces the history-dependent portion of the cost but does not address the per-delta transaction amplification.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions