Repository navigation
docs(protocol): drop the five WebSocket message types WebSocketMessageType does not declare - #17700
Merged
Merged
Conversation
…geType` does not declare
`WebSocketMessageType` is an exhaustive ten-member enum and `BaseWebSocketMessage`
types every message's `type` against it, so a message whose type is outside the
enum parses against no schema in the file. The realtime-protocol page taught
five types that are not members: an in-band handshake (auth, auth_success,
auth_error) and two acknowledgements (subscribed, unsubscribed).
Measured on this tree. All ten declared members show at least one producer or
consumer site under a message-type-context probe over packages/apps/examples;
all five of the above return zero there, and zero in objectui. The contract
agrees: IRealtimeService.handleUpgrade is optional and unimplemented, and
WebSocketConfig.headers is the declared seam for a credential ("Custom headers
for WebSocket handshake"). The five were documentation, not protocol.
Prose only. The page now states the declared vocabulary, puts authentication on
the upgrade request where the declared shapes put it, and shows the declared
`ack` envelope for subscribe and unsubscribe acknowledgements. No schema, no
enum and no behaviour changed; packages/spec/src/api/websocket.zod.ts is
byte-unchanged.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
This was referenced Sep 11, 2026
os-bill
marked this pull request as ready for review
September 11, 2026 16:24
os-bill
deleted the
claude/issue-17184-realtime-protocol-message-types
branch
September 11, 2026 16:54
This was referenced Sep 12, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 17, 2026
…ibeMessageSchema declares (objectstack-ai#17747) Part of objectstack-ai#17184 — the **second half**. The first half (the four undeclared message types) landed as objectstack-ai#17700; this PR does not revisit it. - **Clause-②: no** — this PR puts no new key on any published payload. `packages/spec/src/api/websocket.zod.ts` is **byte-unchanged**: blob `3348dd76745b61e83801e2e8bae48c6756c5a69c` on both sides (`git rev-parse HEAD:<path>` vs `git hash-object <path>`, and an empty `git diff HEAD -- <path>`). Prose only, one file. ## The defect Six subscribe/unsubscribe sites carried every subscription field at the **top level**, in snake_case, where the declared contract nests them and spells them camelCase. `SubscribeMessageSchema` (`websocket.zod.ts:278`–`:281`) declares exactly `type: 'subscribe'` plus a `subscription` object, on top of the three `BaseWebSocketMessage` fields; `EventSubscriptionSchema` (`:102`–`:117`) declares `subscriptionId` / `events` / `objects` / `filters` / `channels`. A fence copied verbatim parses against no schema in the file. The mismatch is **not one thing**, and each axis was verified separately: | page (top level) | declared | axes | |:--|:--|:--| | `subscription_id` | `subscription.subscriptionId` | nesting + snake/camel + **`z.string().uuid()`, so `"sub_1"` does not parse** | | `object`, a string | `subscription.objects`, an array | nesting + singular/plural + scalar/array | | `filter` | `subscription.filters` | nesting + singular/plural | | `events: ["created", …]` | `z.array(EventPatternSchema)` | bare verbs vs patterns — `DataEventType` publishes `data.record.created` | | *absent* | `messageId`, `timestamp` | **both required by `BaseWebSocketMessage`** — an axis not in the card | ### Two page keys map to nothing at all `record_id` and `query` are declared **nowhere** in the protocol. The narrowest scope the contract can express is object + event pattern, so those two sections now say so and move the narrowing to the client rather than showing fields no schema carries. ### Second, independent defect in the same bullet list The page described `filter` as *"Optional filter (same syntax as HTTP API filters)"*. The schema's docblock (`:106`–`:115`) says the opposite in as many words — *"⚠️ **NOT YET ENFORCED** — no runtime evaluates a payload filter. `matchesSubscription` matches on object name and event type only … a subscription carrying `filters` receives every event its patterns match."* The page promised enforcement the contract explicitly disclaims; it now carries that disclaimer. ## Fence census — the round's first deliverable Enumerated on this tree, **not** inherited from the dispatch's page-wide counts. | # | line (pre) | section | type | top-level keys | |--:|--:|:--|:--|:--| | 1 | 256–266 | Subscribe to Object Events | `subscribe` | `type`, `subscription_id`, `object`, `events`, `filter` | | 2 | 379–384 | **Unsubscribe** | `unsubscribe` | `type`, `subscription_id` | | 3 | 404–412 | Subscribe to Specific Record | `subscribe` | `type`, `subscription_id`, `object`, `record_id`, `events` | | 4 | 421–435 | Subscribe to Query Results | `subscribe` | `type`, `subscription_id`, `object`, `query`, `events` | | 5 | 655–662 | Permission Enforcement | `subscribe` | `type`, `subscription_id`, `object`, `events` | | 6 | 679–689 | **Client Reconnection (JS)** | `subscribe` | `type`, `subscription_id` (+ `sub_${Date.now()}`) | ⭐ The card named **four** sections. The census found **six** sites: **Unsubscribe** (objectstack-ai#2) and the **JS client snippet** (objectstack-ai#6) were not on the card's list. objectstack-ai#6 was also missed by a first pass that matched only JSON `"type": "subscribe"` — it uses the single-quoted JS form, and the census was widened to catch it. After the edit all six carry exactly `messageId` / `type` / `timestamp` / `subscription` (or `request` for unsubscribe). ## Fork verdict: **(a) the page is wrong** — every fence Producer evidence on this tree, all six fences landing on the same verdict: - `SubscribeMessageSchema`, `UnsubscribeMessageSchema` and `EventSubscriptionSchema` have **zero** consumers outside `packages/spec`'s own declaration, its unit test and the type-alias pin. - `IRealtimeService.handleUpgrade` is deliberately unimplemented platform-wide — `service-realtime/src/no-channel-route.pin.test.ts:49` asserts it **is not a function**. - `subscription_id` → **0** across **6120** tracked `packages/**` source files (no `dist/`), with four lit controls on the same corpus and the same call: `subscriptionId` **40**, `eventTypes` **27**, `handleUpgrade` **18**, `EventSubscriptionSchema` **13**. - The 660 `record_id` hits are the generic DB-column convention: **zero** files carry both `record_id` and `WebSocket`. Nothing produces or consumes a WebSocket subscribe message, so no top-level field on one can be real. Reading (b) — the schema is incomplete — would need a producer, and there is none. **objectui: reported, not counted.** All WS-subscription probes return 0 there, but the schema-side control is also dark (`subscriptionId` 0, `EventSubscription` 0) — only the generic `useState` (2262) lights, which proves the corpus is real source and nothing more. A zero whose control does not light is not a reading. `cloud` is not checked out here: **NOT MEASURED**. ## Verification - **40/40** derived gates green, on a clean full re-run after the builds. Gate family derived mechanically via `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (it reads the change set from the merge base itself), not hand-listed. - Five gates first returned `PREREQUISITE NOT MET` / "build first" (exit 3 and exit 1) — **NOT MEASURED, not red**. After building `@objectstack/lint...`, `@objectstack/spec` and `@objectstack/client-react...` all five are green. Exit codes captured **before** any pipe. - MDX compiles: `pnpm exec fumadocs-mdx` exit 0. - No generated or tracked artifact moved — `authorable-surface.base.json` included. `git status --porcelain` shows exactly one file. - No control characters: `check:nul-bytes` exit 0, plus a direct `grep -naP` sweep of the edited file. ## `skip-changeset`, and a conflict with the dispatch to flag⚠️ **The dispatch asked for a patch changeset; this PR carries none, and the label instead.** Reporting rather than silently choosing: - `content/docs/**` ships in **no** published package's `files[]` — 0 of 70 publishable packages mention `content/` or `docs/` there, with all 70 shipping `dist` as the positive control. - `@objectstack/docs`, which owns the page, is `private: true` — there is no publishable package to bump. Naming any other package would ship a release note for a package this PR does not touch. - **Precedent on the identical surface**: objectstack-ai#17700, the first half of this very card, merged with no changeset. By the standing criterion — nothing published moves — this is the textbook `skip-changeset` case, which `lint.yml` states in those words. Happy to add a changeset if the maintainer reads it the other way. ## Scope Confined to the subscribe/unsubscribe fences and their Parameters bullet lists. ⛔ Not `websocket.zod.ts`, ⛔ not the message-type sections objectstack-ai#17700 corrected, ⛔ not `content/docs/releases/`, ⛔ no other page. One finding deliberately left out and filed separately: the `type: "event"` fences on this same page carry top-level `subscription_id` / `event` / `data` / `changes` / `reason` where `EventMessageSchema` declares `subscriptionId` / `eventName` / `payload` / `userId` and no `changes` or `reason`. Same defect class, different message family — it is not this round's declared face, and it is not silently widened into. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH --- _Generated by [Claude Code](https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH)_ Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 17, 2026
…geSchema declares (objectstack-ai#18183) Fixes objectstack-ai#17749 Clause-②: no Prose only, one file, and no new key reaches any published payload. `packages/spec/src/api/websocket.zod.ts` is **byte-unchanged** — blob `3348dd76745b61e83801e2e8bae48c6756c5a69c` at the merge base `fffe3e5e7`, at this branch's HEAD, and on disk (`git rev-parse BASE:path`, `git rev-parse HEAD:path`, `git hash-object path` — three identical hashes). ## The premise was checked first, from the runtime — not from either document The card reports snake_case on the page against camelCase in the schema, systematically. That is the signature of two documents written against different wire formats, so before repairing anything: **what does the server actually put on the wire for a realtime event?** **It puts nothing on any wire — there is no realtime transport, and the only shape the platform produces is camelCase.** - `IRealtimeService.handleUpgrade` is deliberately unimplemented platform-wide; `packages/services/service-realtime/src/no-channel-route.pin.test.ts:49` asserts it **is not a function**, and `getChannelRoute` is not implemented either, so discovery reports `enabled: false`. - `EventMessageSchema` has **zero** consumers outside `packages/spec`'s own declaration, its unit test (`src/api/websocket.test.ts`) and the type-alias pin. Nothing constructs an `event` message anywhere. - What the platform *does* publish is the in-process `RealtimeEventPayload` envelope, built at `packages/objectql/src/engine.ts` (`publishDataEvent` / `publishBulkDataEvent`) and handed to `IRealtimeService.publish`. Its keys are `type` / `object` / `payload` / `timestamp`, and `payload` is a `DataEvent` whose keys are `id` / `type` / `object` / `recordId` / `changes` / `after` / `userId` / `organizationId` / `timestamp` — **camelCase throughout**, matching the schema's spelling convention and matching the page on nothing. - There is **no serialisation layer that renames keys**: the only `toSnakeCase` in the tree is `packages/cli/src/commands/generate.ts`, which derives metadata machine names during scaffolding and never touches a message. - `subscription_id` returns **0** across tracked `packages/**` sources (no `dist/`), with three lit controls on the same corpus and the same call — `subscriptionId` **51**, `handleUpgrade` **21**, `EventMessageSchema` **21**. The quoted-literal form (`'subscription_id'` / `"subscription_id"`) is also **0**, with its own same-form control (`'subscriptionId'` / `"subscriptionId"`) at **0** as well — reported as a dark control, so that second reading carries no weight; the unquoted one does. So the schema is the only declared contract for a `type: "event"` message, the producer that exists corroborates its camelCase spelling on every axis, and **the page is the stale side**. The repair stays in docs; ⛔ nothing in `packages/spec` is touched. Confirming the two dispatch readings: `websocket.zod.ts` last moved at `8fd246d27` (2026-09-08T22:18Z), *before* the card, so the schema side is as the card describes. The page last moved at `ec739e671` (2026-09-12T02:18Z), *after* the card's `9bd4344e4b` measurement, so the card's line numbers are stale by construction and were not used. ## Fence census — enumerated by content, on this tree Six, and six is still right. Enumerated by literal match, both fence dialects, because the round that landed objectstack-ai#17747 learned that a JS-quoted `type: 'event'` hides from a JSON-only sweep: - `"type": "event"` (JSON form): **6** hits, at lines 349 / 366 / 387 / 498 / 514 / 737 on the pre-edit tree. - `type: 'event'` (JS form): **0** hits. A widened regex over both quotings and both separators returns the same six lines and nothing else. - Firing control on the same file and the same calls: `"type": "subscribe"` **4**, `type: 'subscribe'` **2**. A second, independent census run with a parser (walk every ```json fence, `JSON.parse` it, keep those whose `type` is `event`) found only **3** — because two of the six carried `//` comments inside the JSON and did not parse at all, and the sixth sat in a ```javascript fence. That disagreement is itself the reason the enumeration had to be by content. | # | pre-edit lines | section | top-level keys | |--:|--:|:--|:--| | 1 | 347–361 | Receiving Events, created | `type`, `subscription_id`, `event`, `object`, `data` | | 2 | 364–382 | Receiving Events, updated | + `changes` | | 3 | 385–396 | Receiving Events, deleted | `type`, `subscription_id`, `event`, `object`, `data` | | 4 | 496–509 | Query Results, entered | + `reason`; **invalid JSON** (a `//` comment) | | 5 | 512–524 | Query Results, exited | + `reason`; **invalid JSON** (a `//` comment) | | 6 | 734–746 | Permission Enforcement | `type`, `event`, `object`, `data`, `reason`; in a ```javascript fence | ## The repair Every fence now carries exactly the declared envelope — `messageId` / `type` / `timestamp` from `BaseWebSocketMessage`, then `subscriptionId` (a real UUID, since the schema declares `z.string().uuid()` and `"sub_1"` does not parse), `eventName`, `object` and `payload`, with `userId` where it reads naturally. The record body moved **inside `payload`**, shaped as the `DataEvent` the engine actually publishes. Three things the page taught that no schema declares are now called out rather than silently dropped: - `changes` is **not** a top-level key. It is real one level down, on the `DataEvent` inside `payload`, and it carries the write's own patch — a flat `{ field: newValue }` map (`publishDataEvent` passes `hookContext.input.data`), **not** the `old` / `new` pairs the page showed. The post-state is `payload.after`. - `reason` is declared nowhere in the protocol. The two `entered_query` / `exited_query` fences described a signal the contract cannot send — the same finding objectstack-ai#17747 recorded in prose while deliberately leaving these two `event` fences alone. They are now ordinary `data.record.updated` messages, with a note that the two are **identical in shape** and that deriving the transition is the client's job, which is exactly what the `query` callout objectstack-ai#17747 added one paragraph above already prescribes. - `subscription_id` / `event` / `data` are not fields of this envelope at all. Wording, section shape (a declaring paragraph, a `Callout type="warn"`, then a field list) and event-name spelling (`data.record.created`, from the closed `DataEventType` enum) follow the neighbouring subscribe/unsubscribe fences objectstack-ai#17747 landed on this page, rather than inventing a second style. Fence 6 also changes language from ```javascript to ```json: its content was a bare JSON object with `//` comments, which is neither valid JS nor valid JSON. ## Verification **The repaired fences parse against the real schema — 0/3 before, 6/6 after.** A throwaway script imported `EventMessageSchema` from the freshly built `packages/spec/dist/api/index.mjs`, walked every ```json fence on the page, and `safeParse`d each one whose `type` is `event`: ``` AFTER (this branch) event fences that are valid JSON: 6 line 351 PARSE OK · 379 PARSE OK · 408 PARSE OK · 553 PARSE OK · 574 PARSE OK · 809 PARSE OK RESULT 6/6 parse against EventMessageSchema BEFORE (merge base fffe3e5, control) event fences that are valid JSON: 3 line 347 PARSE FAILED -> messageId: invalid_type | timestamp: invalid_type | subscriptionId: invalid_type | eventName: invalid_type | payload: invalid_type line 364 PARSE FAILED -> (same five) line 385 PARSE FAILED -> (same five) RESULT 0/3 parse against EventMessageSchema ``` The control leg fires, and it fires on the five axes the card named. The script was deleted after the run — it is a one-off measurement, not a gate (see the note below). Also measured: all **22** ```json fences on the page are now valid JSON (21 before, of which **2** were invalid); the undeclared-key set across the six event fences is now **empty** (before: `subscription_id`, `event`, `data`, `changes`). **Gates — 40 derived, 40 run, 40 green, 0 NOT-MEASURED.** Family derived mechanically with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (it reads the change set from the merge base itself; committed 1, working tree 0, untracked 0), never hand-listed. Each command's exit code was captured into a variable **before any pipe**, then reconciled back: ``` ✓ dispatch-gates --ran: 40 derived famil(ies) accounted for — 40 run, 0 NOT-MEASURED (a DERIVED zero — all 40 recorded an exit code and none of them is 3). ``` Prerequisite builds ran first, under the shared verify lock (`scripts/pm/os-verify-lock.sh`, `VERDICT command-exit 0`): `@objectstack/lint...`, `@objectstack/spec`, `@objectstack/client-react...`. No gate returned exit 3 or a `PREREQUISITE NOT MET` line. **MDX compiles:** `pnpm exec fumadocs-mdx` in `apps/docs` — exit 0, `[MDX] generated files`. No tracked artifact moved: `git status --porcelain` is empty and the diff is one file, 140 insertions / 51 deletions. **No control characters:** `check:nul-bytes` exit 0 (inside the 40), plus a direct sweep of the edited file with a firing control — `grep -naP` over the file exits 1 (no hits) while the identical call over a seeded file exits 0 and prints the hit. **Repo-wide lint is CI's, and the narrowing here is a measurement, not a skip:** the edited file is **outside eslint's population entirely**. `eslint --no-inline-config --format json` on it reports, from eslint's own config resolution, `File ignored because no matching configuration was supplied`, and every `files:` glob in `eslint.config.mjs` names only `{ts,tsx,mts,cts,js,jsx,mjs,cjs}`. Positive control: the same invocation on `packages/services/service-realtime/src/in-memory-realtime-adapter.ts` reports 1 file with 0 messages and no ignore notice. So this diff moves zero files eslint examines, and no invariance argument about type-aware linting is needed. Every number above was taken on the final commit, `0bae112e0`. ## skip-changeset Nothing published moves, measured rather than asserted: across **70** publishable packages declaring a `files[]`, **0** name `content/` or `docs`, with all **70** naming `dist` as the positive control. `@objectstack/docs`, which owns the page, is `private: true`, so there is no publishable package to bump — and naming another would ship a release note for a package this PR does not touch. Same surface and same reading as objectstack-ai#17700 and objectstack-ai#17747, both of which landed with no changeset. ## Scope, and one thing deliberately not built Confined to the six `type: "event"` fences and their surrounding prose. ⛔ Not `websocket.zod.ts` (byte-identical, proved above), ⛔ not the subscribe/unsubscribe fences objectstack-ai#17747 corrected, ⛔ not `content/docs/releases/`, ⛔ no other page. **No gate was invented for this card, and here is why, measured.** Two instruments read fenced examples in this repo: `check:skill-examples` compiles ts/tsx/typescript fences marked `{/* os:check */}`, and `check:yaml-examples` `safeParse`s yaml/yml fences marked `os:check-yaml DECL` against a named spec schema. **A ```json fence is in neither instrument's population** — this page carries 0 markers of either kind. Making these six fences mechanically checkable therefore means extending a gate, which is out of scope here; it is recorded in the acceptance notes below instead. ## Acceptance notes Noted while working, ⛔ not filed and ⛔ not fixed here: - **A JSON fence is in no example-checking gate's population.** `check:yaml-examples` already validates yaml fences against a named schema and its own header says corpus-wide tagging is a follow-up card; extending the same marker mechanism to `json` would have caught this defect and objectstack-ai#17184's automatically. Successor: that gate's own follow-up lane. - **`"details": { "subscription_id": "sub_1" }`** in the `error` fence near the top of this page. Not class (a): `ErrorMessageSchema.details` is `z.unknown().optional()`, so the example parses — it is a spelling inconsistency inside a free bag, in the `error` family objectstack-ai#17186 owns, not a defect. Left alone. - **The `/api/v1/realtime/events` event-log section** shows `subscription_id` / `event` / `reason` too, but it is an HTTP debug endpoint with no schema anywhere and the page already carries an explicit "**not implemented** … planned transport tooling" callout directly above it. Nothing to be wrong against. Left alone. - **The Event Types table** still lists the bare verbs `created` / `updated` / `deleted` / `restored` while the fences now spell `data.record.*`. That is the `eventName` **vocabulary** question, which objectstack-ai#17749 explicitly puts out of scope and which is already tracked in `packages/spec/liveness/realtime_subscription.json` — note that `restored` is in no enum at all. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk --- _Generated by [Claude Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_ Co-authored-by: Claude <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.
Part of #17184 — the message-type half of that card.
Prose only, one file:
content/docs/protocol/kernel/realtime-protocol.mdx. No schema, no enum, no behaviour;packages/spec/src/api/websocket.zod.tsis byte-unchanged and was read-only for this round.The fork, and the branch this PR takes
The card offers two readings: (a) the page documents a protocol the platform does not implement, or (b) the four types are real and the declaration is missing them. Every measurement below points at (a), for all of them, individually. Nothing here adds a member to a published enum.
Prerequisite readings — taken on
origin/mainat88a933088e, anchored by contentWebSocketMessageTypestill declares exactly ten members. Read live off the built package rather than inherited from the card:WebSocketMessageType.optionsoverpackages/spec/dist/api/index.mjsprints["subscribe","unsubscribe","event","ping","pong","ack","error","presence","cursor","edit"].BaseWebSocketMessagestill typestypeagainst that enum — the reading the whole argument rests on.packages/spec/src/api/websocket.zod.ts:const BaseWebSocketMessage = z.object({ … type: WebSocketMessageType.describe('Message type'), … }), andWebSocketMessageSchemais az.discriminatedUnion('type', …)over exactly those ten branches. The premise stands.type-value site on the page rather than a lookup of the card's list. That census turned up five, not four: the card'sauth/auth_success/auth_error/subscribed, plusunsubscribedin the Unsubscribe section, which is the same defect in the same file and is corrected here too.Per-type verdict — each judged on its own evidence
The probe is one regex applied uniformly to all fifteen values (ten declared, five undeclared) over the tracked files of
packages,apps,examplesin a worktree with no build output in it:(type\s*[:=]+\s*|case\s+)['"]X['"].authauth_successauth_errorsubscribedunsubscribedThe lit control, and it lights for all ten: under the same probe every declared member returns a non-zero count —
subscribe2,unsubscribe1,event2,ping3,pong2,ack2,error9,presence1,cursor1,edit1. An instrument that returns zero for everything proves nothing; this one separates the two sets cleanly, ten from five.Two supporting readings, neither load-bearing on its own:
content/docsreturns one line forauth_success|auth_error, and it is not a hit:packages/spec/src/identity/protocol.ts:136isOAUTH_ERROR: 'oauth_error', an OAuth error-code constant matched as a substring. Its control,ErrorMessageSchemaon the same corpus, returns 13 occurrences across 6 files.objectuicheckout returns 0 for all five — but its control does not light there (subscribe,ping,pong,ackare all 0 too), so that repo carries no WebSocket message producer at all and those zeros carry no weight about the five specifically. Reported rather than counted.cloudis not checked out in this container, so it is not measured.The declared contract agrees with the verdict independently of any grep:
IRealtimeService.handleUpgradeis optional and unimplemented across the open framework (#2462, #3197), the module note onwebsocket.zod.tssays "no WebSocket server is mounted", andWebSocketConfigSchema.headersis described as "Custom headers for WebSocket handshake" — the declared seam for a credential is the HTTP upgrade, not an in-band message. Soauthis not an omission from the enum; authentication is declared somewhere else. Likewisesubscribed/unsubscribedare not omissions:AckMessageSchema(type: 'ack',ackMessageId,success, optionalerror) is the acknowledgement the protocol declares.The corrected prose
BaseWebSocketMessagepinstypeto them and that a message outside the enum matches no branch of the discriminated union, and carries a callout naming the five types the page used to teach and why they were not protocol.WebSocketConfig.headersintoIRealtimeService.handleUpgrade(request), and notes that a refused upgrade never becomes a WebSocket — so there is no success or failure message to wait for. The three handshake fences are gone.ackenvelope, withackMessageIdechoing the acknowledged message'smessageId.{ type: 'auth' }— in Establishing Connection, in the reconnection client, and under Security Considerations — no longer do. The reconnection client'sauthenticate()method and itstokenconstructor argument are gone with it; that example is about backoff.Verification
Instrument: re-run the same content census over the edited page and
safeParseeach distinct value against the built enum.The control is the point: the same call that reports every remaining site as declared still reports the five removed types as undeclared, so the zero above is a reading and not a dead instrument. Before the edit the same census printed five undeclared values across eight sites.
packages/specwas built under the shared verify lock (VERDICT command-exit 0) so the parse reads a freshdist/, not a stale one.grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'over the edited file returns nothing.A repo-wide lint run is CI's; this diff touches one
.mdxfile and no package, so the affected-package build/test closure is empty.Changeset
skip-changeset. The rule: a changeset is owed when the PR moves something published, and what a package publishes is what itsfiles[]actually ships. The measurement: of 81package.jsonfiles on this tree, 11 areprivate: trueand 70 are publishable; every one of the 70 declares an explicitfiles[](none ships its whole directory), none of those arrays mentionscontent, and none of the 70 lives at the repo root — so no published tarball can containcontent/docs/**by any route. Positive control for the reader:@objectstack/specreads back["dist","json-schema","liveness","prompts","llms.txt","README.md","src/**/*.zod.ts","CHANGELOG.md","api-surface","spec-changes.json"], so it is reading real arrays.content/is consumed only byapps/docs, which isprivate: true.验收备注
subscription_id,object,events,filter— and those spellings survive on the remaining subscribe fences (Subscribe to Object Events, Subscribe to Specific Record, Subscribe to Query Results, Permission Enforcement). They use a declaredtype, so they are the field-shape half of [finding] realtime-protocol.mdx documents four WebSocket message types thatWebSocketMessageTypedoes not declare (auth,auth_success,auth_error,subscribed) #17184 rather than the message-type half, and rewriting all of them ontoSubscribeMessageSchema({ messageId, type, timestamp, subscription: { subscriptionId, events, objects?, filters?, channels? } }) is a materially larger edit on a surface this round was not dispatched to. Naming it here so the seat can decide whether it is a follow-up round or a second card.unsubscribedwas not in the card's list of four. It is the same defect class, in the same file, inside the declared file face of this round's claim, and it is a mechanical correction of a form already pinned by the other four — so it is corrected here rather than filed. Evidence:"type": "unsubscribed"in the Unsubscribe section; 0 probe hits;WebSocketMessageType.safeParse('unsubscribed')returnssuccess: false.🤖 Generated with Claude Code
https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Generated by Claude Code