fix(claude): drop empty streaming-seed assistant lines on projection#141
Open
bdelanghe wants to merge 2 commits into
Open
fix(claude): drop empty streaming-seed assistant lines on projection#141bdelanghe wants to merge 2 commits into
bdelanghe wants to merge 2 commits into
Conversation
bdelanghe
commented
Jul 23, 2026
Claude Code streams an assistant message as several JSONL lines — the
first an empty "seed" (text: "") superseded by the real-text line, both
sharing one message.id. The projector wrote that seed back as
content:[{"type":"text","text":""}], an API-invalid message: on the next
turn of a *resumed* session Claude replays the whole transcript and
Anthropic rejects the empty text block with
400 messages: text content blocks must be non-empty
so a resumed session couldn't take a second turn.
The projector now skips any assistant turn with no text, thinking,
tool-uses, delegations, or file-mutations (and no attached tool-result
events), re-linking the following turn's parentUuid to the dropped seed's
parent via the existing parent_rewrites machinery so the uuid chain stays
intact. The group token total is re-expanded onto the surviving line as
before.
Verified end-to-end by re-exporting a real captured session: empty text
blocks 1 -> 0, chain re-linked past the seed. toolpath-claude 0.12.0 ->
0.12.1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bdelanghe
force-pushed
the
bdelanghe/fix-empty-seed-assistant-projection
branch
from
July 24, 2026 02:45
c7caa90 to
2c5f8b5
Compare
This was referenced Jul 24, 2026
The projector's seed-drop check was an inline five-way conjunction over turn content fields. Promote the intrinsic part to `Turn::is_content_empty()` on toolpath-convo — a reusable predicate (no text/thinking/tool-uses/delegations/file-mutations) — and keep the projector-specific tool-result-events check at the call site. toolpath-convo 0.11.1 → 0.12.0 (additive: new public method). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
|
Good call — extracted it to let is_droppable_seed = turn.is_content_empty()
&& !tool_result_events_by_parent.contains_key(&turn.id);
|
bdelanghe
pushed a commit
to bdelanghe/toolpath
that referenced
this pull request
Jul 24, 2026
… review) Same cleanup the empathic#141 reviewer asked for, applied here where the projector's seed-drop check lives in its improved (root-of-chain) form: promote the intrinsic predicate to Turn::is_content_empty() on toolpath-convo and keep the tool-result-events check at the call site. toolpath-convo 0.11.1 → 0.12.0 (additive), with a unit test. Co-Authored-By: Claude Fable 5 <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.
Problem
Resuming a projected Claude session works, but the next turn dies with:
Root cause
Claude Code streams an assistant message as several JSONL lines — the first an empty "seed" (
text: "") superseded by the real-text line, both sharing onemessage.id. The Claude projector wrote that seed back ascontent:[{"type":"text","text":""}]. That message is API-invalid: on the next turn of a resumed session Claude replays the whole transcript to Anthropic, which rejects the empty text block. The bug was inbuild_assistant_content's text-only fast path, which (unlike the tool/thinking path) had no empty-text guard.Fix
The projector now skips content-empty assistant turns (no text/thinking/tool-uses/delegations/file-mutations, and no attached tool-result events) and re-links the following turn's
parentUuidto the dropped seed's parent — reusing the existingparent_rewritesmachinery, so the uuid chain stays intact. The group's token total is re-expanded onto the surviving line as before.Verification
test_projector_skips_empty_text_seed_and_relinks_chain(failed before, passes after).toolpath-claudesuite: 230 passed, 0 failed. Clippy clean.1 → 0; the seed step is gone and the surviving assistant line re-parents to the user turn.test_projector_reexpands_group_usage_onto_every_linetest that had encoded the old emit-empty-line behavior.Version:
toolpath-claude0.12.0 → 0.12.1 (Cargo.toml, workspace dep, crates.json, CHANGELOG).Versioning
toolpath-claude0.12.1 → 0.12.2 — 0.12.1 was taken by main'sClaudeConvo::session_chainbump (#133), so the empty-seed fix gets its own patch.Stack (review bottom-up): #141 empty-seed projection fix → #142 opencode test isolation → #145
path resume --remote.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.