feat(claudecode): an unmodelled line cannot fail the run, and an unreadable one is an event - #8
Merged
Conversation
Decode unmarshalled the whole event before reading its type, which made every field's shape load-bearing on every line — including lines nothing here reads. A bookkeeping event whose `message` is a string rather than an object failed the run before the type switch could skip it, so the tolerance documented on Decode was unreachable for exactly the lines it was written for. Claude Code emits such a line on context compaction, which is why long runs were the ones that died, and why the failure looked intermittent. The type is now read on its own, from a shape nothing else on the line can break. A modelled line whose shape is unreadable is skipped rather than fatal: these events drive progress display, while the outcome comes off the terminal line, so an unreadable one costs a caller a tool call it was going to watch rather than the result it was waiting for. The terminal line stays strict — reporting success off a result nobody could parse would invent a verdict.
|
A line whose type the provider models but whose shape it cannot read was returned as the zero event, which the driver skips. That is indistinguishable from a turn where nothing happened: a caller rendering the run has nothing to show, and one diagnosing a CLI whose output has moved has nothing to look at. EventKindUnreadable carries the undecoded line in Raw and the reason in Text. It is distinct from EventKindUnknown, which stays skipped, because the two mean different things — a line nobody models is noise, while one the provider should have understood is a signal. It is not an error, because the run's outcome arrives on the terminal line regardless, and failing a run over a line that only drives display would trade the result for the progress bar.
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.
v0.4.0's fix formessage.contentwas incomplete. It corrected one shape; the defect is structural and one level up.Decodeunmarshalled the whole event before reading its type, so every field's shape was load-bearing on every line — including lines this package never reads. A bookkeeping event whosemessageis a string rather than an object failed the run before the type switch could skip it, which makes the tolerance documented onDecodeunreachable for precisely the lines it was written for:That promise could not hold, because the unmarshal ran first.
Why it looked intermittent
Claude Code emits a system line on context compaction:
{"type":"system","subtype":"compact_boundary","message":"context compacted"}messageis a string there. Long runs compact; short ones don't.agtk memory curatedied twice against a store large enough to compact, while ordinary runs were fine — I captured live streams from a plain prompt, a tool-using prompt and a subagent run, and none of them reproduced it.The change
The type is read on its own, from a shape nothing else on the line can break. Then:
result— stays strict. Reporting success off a result nobody could parse would invent a verdict.Tests
Each was confirmed to fail before the change, except the strict-
resultone, which passed already and is a guard: a fix that made everything tolerant would have silently taken the verdict with it.Note on the previous PR
The
messageContenttype from #7 is still doing useful work — it extracts assistant text from the string form rather than discarding it — but it should not have been the only line of defence. I fixed the shape I could reproduce instead of establishing why an unreadable line could fail a run at all.An unreadable line is an event, not silence
Returning the zero event for a shape the provider cannot read is itself a form of suppression:
EventKindUnknownis what the driver skips, so a caller never learns the line existed. That is indistinguishable from a turn where nothing happened — which blocks anyone building a UI on this driver, and hides exactly the signal that says a CLI's output has moved.EventKindUnreadablecarries the undecoded line inRawand the decode error inText. It is:EventKindUnknown, which stays skipped — a line nobody models is noise; one the provider should have understood is a signal;A driver-level test proves it reaches the caller rather than asserting the constant differs; mutating
Streamto filter it alongsideEventKindUnknownmakes that test fail.