Skip to content

claude-sdk-oauth: a content-less user message forces a full-history re-send every time it disappears #790

Description

@1vivy

What happened?

sentMessages() hashes every user and toolResult message. A user message whose content array is
empty carries nothing to transmit and is transient — present for one provider call, gone by the next.

Because it is hashed, its removal shifts every later index, commonPrefixLength stops short of
sentCount, and the turn resolves to sent_stream_diverged: the session forks and re-sends the whole
history even though the conversation never changed.

Captured on 2026.8.9-2 from two consecutive provider calls of one session, both 203 messages:

call N      [200] toolResult bash  len=159   sha b518dd86
            [201] user             len=0     sha 12ae32cb   <- content: []
            [202] user             len=8194  sha 5c1ea814

call N+1    [200] toolResult bash  len=159   sha b518dd86
            [201] user             len=8194  sha 5c1ea814   <- shifted up one slot
            [202] toolResult bash  len=3191  sha aec978fe

Index 200 is byte-identical. Only the empty message disappears, and the next decision is
flatten / sent_stream_diverged at 203 messages.

Cost on one affected turn: cacheRead collapsed 582,409 -> 61,594 with cacheWrite 524,339, roughly 8x
its neighbouring turns. That session logged 12 sent_stream_diverged events in 12 minutes.

This is independent of #691 — it reproduces on 5275da1f5, after #751 merged.

Steps to reproduce

#791 carries the regression. To see the failure on unpatched main, check the PR out and revert only
the production change:

gh pr checkout 791
git checkout main -- packages/coding-agent/src/core/extensions/builtin/claude-sdk-oauth/session-sync.ts
cd packages/coding-agent
npx vitest run test/suite/regressions/790-claude-sdk-oauth-empty-user-continuity.test.ts

Observed:

× excludes a content-less user message from the transmitted set
    expected length 2, received 3
× stays a delta when a transient content-less user message disappears
    - "kind": "delta",   "reason": null
    + "kind": "fork",    "reason": "sent_stream_diverged"

Tests  2 failed | 2 passed (4)

Expected: Tests 4 passed (4).

Restore the fix with git checkout - -- packages/coding-agent/src/core/extensions/builtin/claude-sdk-oauth/session-sync.ts.

Expected behavior

A transient message carrying nothing to transmit should not participate in the continuity hash, so an
unchanged conversation stays a delta.

Genuine divergence must keep failing closed: a real rewrite of already-sent history should still report
sent_stream_diverged. The two passing controls in that test file cover both directions.

Version

@code-yeongyu/senpi 2026.8.9-2 (observed) · reproduced on main @ 5275da1f5
@anthropic-ai/claude-agent-sdk 0.3.220
provider claude-sdk-oauth · model claude-opus-5 · thinking high

I have a fix in #791. Written with an LLM; the reproduction, measurements, and verification are mine.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions