Repository navigation
Keep older apps updating when a newer node sends unknown events - #768
Merged
Merged
Conversation
A client decoded every server-to-client stream with closed unions. When a newer node sent an event type, stream item kind or update kind this build did not know (thread.trashed, worktree.relocated and the like), decoding failed with a SchemaError. wsTransport does not retry a SchemaError, so the subscription ended and the thread, shell, config or terminal view stopped updating until the app reloaded. Older App Store, desktop and CLI builds connected to updated nodes hit this. Contracts gain ForwardCompatibleUnion and UnknownUnionMember (after T3 Code ca3f9738c3, pingdotgg/t3code#15951). A member whose tag is unknown decodes to an UnknownUnionMember placeholder; a known tag whose payload is broken still fails; a placeholder cannot be encoded. Unlike T3, the placeholder can keep fields every member has: an unknown orchestration event keeps its sequence, aggregateKind and aggregateId, and an unknown shell item its optional sequence, so resume cursors move past it instead of replaying it. The known members stay one nested union that dispatches on the tag, so a known member decodes once on Effect's single-candidate fast path (a flat union cost about 35% on small events; nested is within a few percent of the strict union). Only what a node sends a client is forward-compatible: thread and thread window stream items with their events (OrchestrationWireEvent), shell stream items (OrchestrationShellStreamWireItem), replay results, server config stream events and terminal events. The event store, the decider, client commands and the server-built OrchestrationEvent, OrchestrationShellStreamItem, ServerConfigStreamEvent and TerminalEvent stay strict. The client runtime skips placeholders: thread subscriptions advance resumeFromSequence past unknown events without applying them, the shell connection hands an unknown shell event's sequence to a new skipShellEvent supervisor step (queued in order with the events around it), the shell snapshot assembler passes placeholders through, and config and terminal events are dropped before they reach the stores. Each skip records a per-environment environmentNodeNewerAtom flag (one warning per environment) for an "update this app" hint. The ryco remote CLI skips them too. Server integration tests assert the server never sends a member its own contract does not know. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Say plainly that no UI reads environmentNodeNewerAtom yet. The "update this app" hint that will read it is a follow-up. - Route every client skip through one helper, skipNewerNodeContent, so the four copies of "is it unknown, then record it" are gone. recordNewerNodeContent is now private. - Add a guard test: every stream RPC in the hosted group must be listed on purpose, either as forward-compatible or as strict. A new stream can no longer slip in with a closed union unnoticed. - Note on the shell wire item that `sequence` always means a shell event sequence. A new non-event kind must name its own field, or older clients would move their cursor to it. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
sak0a
force-pushed
the
fix/forward-compatible-wire-unions
branch
from
October 23, 2026 15:25
5ef4885 to
ab72005
Compare
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.
What Changed
baseSchemas.ts:ForwardCompatibleUnion,UnknownUnionMemberandisUnknownUnionMember.UnknownUnionMemberplaceholder (tag,value).sequence,aggregateKindandaggregateId. An unknown shell item keeps its optionalsequence. Resume cursors can then move past it instead of replaying it.OrchestrationWireEvent)OrchestrationShellStreamWireItem)replayEventsandreplayEventsPageresultsOrchestrationEvent,OrchestrationShellStreamItem,ServerConfigStreamEventandTerminalEvent.resumeFromSequencepast an unknown event without applying it.skipShellEventsupervisor step. The step is queued in the shell batcher in order with the events around it. Web and mobile both wire it.skipNewerNodeContent. It sets a per-environmentenvironmentNodeNewerAtomand logs one warning per environment. No UI reads the flag yet (see Follow-ups).ryco remoteCLI skips placeholders too.Why
Clients decoded every server-to-client stream with closed unions. When a newer node sent an event type this build did not know (
thread.trashed,worktree.relocatedand the like), decoding failed with a SchemaError.wsTransportdoes not retry a SchemaError, so the subscription ended. The thread, shell, config or terminal view then stopped updating until the app was reloaded. Older App Store, desktop and CLI builds that connect to updated nodes hit this.This follows T3 Code ca3f9738c3 (pingdotgg/t3code#15951), with two differences:
thread.message-sentevents.Testing
After rebasing onto main and applying the review fixes:
bun run check: format and lint pass with 0 errors, andtypecheck:fastpasses in 12 of 12 packages.bun run --cwd packages/contracts test -- src/forwardCompatibleWire.test.ts src/baseSchemas.test.ts: 40 passed. I also removed one stream from the strict list once to check that the new guard fails.bun run --cwd packages/client-runtime test -- src/connection/connection.test.ts src/connection/supervision.test.ts src/rpc/shellSnapshotAssembly.test.ts src/rpc/wsTransport.test.ts: 134 passed.RYCO_TEST_MAX_WORKERS=4 bun run check:changed: exit 0. Format and lint passed, the TS7 + Effect typecheck passed in 12 of 12 packages, the related tests passed in 10 of 10 packages, and every read-from-disk suite passed.Before the review fixes, on the previous main:
bun run --cwd packages/contracts test: 582 passed.bun run --cwd packages/client-runtime test: 3349 passed.bun run --cwd apps/mobile test: 2089 passed.bun run --cwd apps/server test -- src/server.test.ts: 141 passed.bun run --cwd apps/server test -- src/test/WsTestClient.test.ts src/hubConnector/HostedRelaySessionIntegration.test.ts src/ws/context src/cliRemote: 126 passed.Follow-ups
environmentNodeNewerAtom, in the web desktop tier and in mobile. Where it goes is still open: the ChatView version-mismatch banner, Connections settings, or Diagnostics. ChatView is also being changed by a parallel batch.ForwardCompatibleUnionthe same way.ForwardCompatibleOptionalandForwardCompatibleArraywould be the next step.ryco remoteCLI skips unknown members silently. It could print a one-line notice that the remote is newer.🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need.