Skip to content

fix(claude-sdk-oauth): ignore content-less user messages in continuity hashes - #791

Open
1vivy wants to merge 1 commit into
code-yeongyu:mainfrom
1vivy:fix/claude-sdk-oauth-empty-user-message-continuity
Open

fix(claude-sdk-oauth): ignore content-less user messages in continuity hashes#791
1vivy wants to merge 1 commit into
code-yeongyu:mainfrom
1vivy:fix/claude-sdk-oauth-empty-user-message-continuity

Conversation

@1vivy

@1vivy 1vivy commented Aug 10, 2026

Copy link
Copy Markdown

Summary

  • ignore user messages with no content blocks when computing the claude-sdk-oauth sent-stream hashes
  • add a regression covering the transient empty message, a plain append, a genuine rewrite, and the filter itself
  • record the change in the extension's changes.md

Root cause

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

Because it is hashed, its removal shifts every later index, commonPrefixLength stops short of
sentCount, and decideNativeContinuity resolves the turn to sent_stream_diverged — forking the
session and re-sending 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, about
8x its neighbouring turns. The same session logged 12 sent_stream_diverged events in 12 minutes.

Independent of #691 — this reproduces on 5275da1f5, well after #751 merged.

Fix

  • sentMessages() drops user messages whose content array is empty
  • tool results are never filtered: an empty tool result is a real observation, and its toolCallId
    and toolName stay hash-significant
  • no change to the divergence decision table, the fork boundary, or the transmitted prompt for any
    message that carries content

Why this cannot be an extension: the transmitted set is computed inside the builtin provider before
session-registry continuity is decided, and no extension hook can replace it. A changes.md section
records the modification and its expected merge-conflict zones.

RED → GREEN

RED

Against 5275da1f5 with only the test added:

× 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)

GREEN

Tests  4 passed (4)

The two control cases pass in both runs, so the guard still fails closed: a plain append stays a
delta, and a genuine rewrite of already-sent history still reports sent_stream_diverged.

Verification

npm run check                                   -> exit 0
npx vitest run test/suite/regressions/790-claude-sdk-oauth-empty-user-continuity.test.ts
                                                -> 4 passed
npx vitest run test/claude-sdk-oauth-*.test.ts  -> 35 files, 314 passed, 3 skipped

A deterministic container reproduces the bug and verifies the fix with no credentials and no tokens
spent. It clones 5275da1f5, runs the regression unpatched, applies the production patch, and
re-runs; it exits 0 only when the unpatched tree fails specifically with sent_stream_diverged and
the patched tree reports all four tests passing:

== ARM 1: unpatched ==
  Tests  2 failed | 2 passed (4)
  +   "reason": "sent_stream_diverged"

== ARM 2: with fix applied ==
  Applied patch ... cleanly.
  Tests  4 passed (4)

PASS: reproduced on 5275da1f5 and verified the fix resolves it.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 794298f7f5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@@ -0,0 +1,112 @@
import type { Context } from "@earendil-works/pi-ai";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Name the regression after issue 790

This commit explicitly fixes issue #790, but the new regression uses the placeholder 0000, defeating the repository's issue-to-test traceability convention. Rename it to 790-claude-sdk-oauth-empty-user-continuity.test.ts.

AGENTS.md reference: packages/coding-agent/test/AGENTS.md:L40-L40

Useful? React with 👍 / 👎.

…y hashes

A user message with no content blocks is transient: present for one provider call and gone by the next. Hashing it shifted every later index, so the next turn reported sent_stream_diverged and re-sent the whole history despite an unchanged conversation.

Filter content-less user messages out of the transmitted set. Tool results are untouched, so genuine rewrites of already-sent history still diverge.
@1vivy
1vivy force-pushed the fix/claude-sdk-oauth-empty-user-message-continuity branch from 794298f to 5baf13f Compare August 10, 2026 06:11
@1vivy

1vivy commented Aug 10, 2026

Copy link
Copy Markdown
Author

Good catch — renamed to 790-claude-sdk-oauth-empty-user-continuity.test.ts to match the <issue>-<slug>.test.ts convention. The 0000- prefix was written before the issue existed. The changes.md reference and the repro command in #790 now point at the new path.

npm run check and the regression are green on the amended commit (4 passed).

@1vivy

1vivy commented Aug 10, 2026

Copy link
Copy Markdown
Author

Verification note, since I can't produce a CI run from a fork.

I mirrored ci.yml in a container — Node 24, the same apt set (libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev fd-find ripgrep, fdfind symlinked to fd), npm ci --ignore-scripts, non-root user — and cloned the pushed commit.

Static stage on this commit:

install  PASS      check  PASS      build  PASS

coding-agent shard 1/3 is red in that container, so I ran it against untouched upstream main in the same image:

my commit      Test Files  2 failed | 282 passed | 2 skipped (286)
5275da1f5      Test Files  2 failed | 282 passed | 2 skipped (286)

Identical, so those failures are pre-existing here and not from this change. One of the three original failures was my own container artifact — test/mcp/log-redaction.test.ts expects an unwritable sink, which root defeats; it passes once the container runs as non-root.

Scoped results on this commit:

npm run check                                   exit 0
790-claude-sdk-oauth-empty-user-continuity      4 passed
test/claude-sdk-oauth-*.test.ts                 35 files, 314 passed, 3 skipped

I'm not claiming a full green npm test locally — it isn't, on main either in this environment. Happy to rerun anything specific.

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