fix(clients): older clients skip event types they don't know instead of breaking - #13941
Conversation
…of breaking
A thread stream event with a type the client build does not know used to
fail the chunk decode. The RPC client turns that into a defect, which ends
the thread subscription, and resubscribing replays the same event.
The thread stream item union now has a decode-only fallback arm. It takes an
`event` item whose `type` is not a known domain event type and decodes it as
`{ kind: "unknown-event", sequence, eventType }`. A known type with a payload
that does not decode still fails. The client-runtime reducer skips these
items with a debug log and still advances its resume cursor past them.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
| Effect.annotateLogs({ | ||
| environmentId, | ||
| threadId, | ||
| eventType: item.eventType, |
There was a problem hiding this comment.
eventType is an unchecked string from the wire, so logging it can expose arbitrary payload text or produce unbounded annotations. Could you log a safe diagnostic such as its length instead?
| eventType: item.eventType, | |
| eventTypeLength: item.eventType.length, |
Posted via Macroscope — Effect Service Conventions
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: unavailable · PR result: Scenario and decoded snapshot size10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — The PR is a focused compatibility fix that safely skips unknown events and preserves resume sequencing, with targeted tests. Human review is warranted because the new debug logging records unchecked event-type text from the wire, which may expose sensitive content despite truncation. You can add or adjust custom eligibility rules. Learn more. |
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
| environmentId, | ||
| threadId, | ||
| // Bounded: the type comes from a newer server and is not validated here. | ||
| eventType: item.eventType.slice(0, 64), |
There was a problem hiding this comment.
Truncating eventType bounds its size but still puts unchecked wire text (potentially payload or secrets) into logs. Could you record only a safe diagnostic instead?
| eventType: item.eventType.slice(0, 64), | |
| eventTypeLength: item.eventType.length, |
Posted via Macroscope — Effect Service Conventions
…of breaking (pingdotgg#13941) Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every V2 PR that adds a domain event type (for example
run.background-work-cancelledin #13895) breaks older web, desktop, and mobile clients.OrchestrationV2ThreadStreamItemis a strict union, so an unknownevent.typefails the whole chunk decode.What happens today. The Effect RPC client decodes each stream chunk with
Effect.orDie, so a decode failure is a defect that ends thesubscribeThreadstream. In client-runtime,subscribeDynamicreports it throughonDefect("Could not synchronize the thread.") and does not retry. The thread stays on its cached data until the next session or foreground resubscribe. That resubscribe starts from the sameafterSequence, so the server replays the same event and it fails the same way. On an older mobile build the thread never goes live again.What changed
OrchestrationV2ThreadStreamItemgets a decode-only fallback arm, placed after the knowneventarm. It accepts{ kind: "event", sequence, event: { type } }only whentypeis not a known domain event type, and decodes it to{ kind: "unknown-event", sequence, eventType }. A known type whose payload does not decode still fails, so real contract bugs are still caught. Encoding this arm is forbidden, and the server never builds it.unknown-eventlike an event for the sequence cursor. It skips the item with a debug log and still moveslastSequencepast it, so resubscribe and catch-up math stays correct, even when the unknown event is the last one in a batch.Scope decisions
OrchestrationV2DomainEvent,OrchestrationV2DomainEventJsonandOrchestrationV2StoredEventare unchanged. The event store and provider ingestor still reject unknown types.kindvalues have been stable since June:project.updatedandproject.removedalready exist, and the only later addition was thesynchronizedmarker, which is negotiated throughshellResumeCompletionMarker. Project domain events do not reach the shell wire; the server maps them toproject.updated/project.removed. refactor(server): commit project commands without the V1 engine #13896 adds project event types, but it touches onlyapps/serverand the shell item kinds stay the same. The archived shell stream has the same shape and the same reasoning applies.apps/mobileusescreateOrchestrationEnvironmentAtomsandmakeEnvironmentThreadStatefrom client-runtime over the sameWsRpcGroupclient, so this fix covers web, desktop, and mobile.Clients only benefit from this once they ship with it. Builds already in users' hands still break on new event types.
Verification
packages/contracts:vp test run src/orchestrationV2.test.ts(28 passed). The new test decodes, through the RPC JSON codec, a known event, then arun.background-work-cancelledevent, then another known event. It checks that the middle item becomesunknown-eventwith its sequence, and that a known type with a broken payload still throws.packages/client-runtime:vp test run src/state/threads-sync.test.ts src/state/threads-atoms.test.ts src/state/shell-sync.test.ts(63 passed). The new test feedsmessage.updated, then an unknown event, thenthread.metadata-updated, then a trailing unknown event, thensynchronized. It checks that both known events applied, the thread went live with no error, and a replacement session resubscribes withafterSequenceequal to the trailing unknown event's sequence.SchemaErroron the unknown type, and the reducer test timed out because the stream never went live.apps/server:vp test run src/orchestration-v2/ThreadTransportPerformance.test.ts src/orchestration-v2/ThreadLiveEventCoalescer.test.ts(13 passed).tsc --noEmitfor contracts, client-runtime, web, mobile, and server: no errors.vpr knip:check: clean.vp lintandvp fmton the touched files: clean.Model: Claude Opus 5.5 (Claude Code)
🤖 Generated with Claude Code