Review follow-up on the client build/channel work. Fourteen findings; the
substantive ones:
The v3 body fallback took client_playback_context.app_version whenever the
header was absent. The web player sends the literal "web" there and sends no
X-Silo-Client, so every browser session would have stamped client_version="web"
— the one field the contract promises is semver and the field a future
minimum-version gate has to key on. client_playback_context carries no app name,
so the body can never identify a nameless client anyway; the fallback now
applies only to a client that sent X-Silo-Client, and a test pins the "web"
case.
An over-long app_build or app_channel in the start body failed the whole request
with 400 while the same value in a header was silently clamped — an opaque
diagnostic label could refuse playback. validateCapabilitiesV3 now clamps both
with the same helper the header path uses, which is what the docs already
claimed.
Route events posted out of band resolved identity from headers only, so a client
reporting its build in the start body attributed plan_selected to a build and
every later event of the same attempt to none. They now fill empty fields from
the session, as the replan path already did.
playbackClientFullDisplayName discarded build and channel whenever the client
reported no name, so the new Client card could never show a build for a
user-agent-labelled session. It now qualifies whatever label the compact
formatter resolved, which also drops its duplicated name+version assembly.
normalizeClientMetadataValue truncated by bytes; a multi-byte header value cut
mid-rune yields invalid UTF-8, which Postgres rejects — and the per-node session
upserts share one transaction, so one malformed client string would fail that
whole node's sync. It now clamps on a rune boundary.
replan-request.schema.json never got app_build/app_channel even though
ReplanRequestV3 reuses ClientPlaybackContextV3 and validates the same bounds. A
new contract test asserts every $def the two request schemas share is identical,
so the copies cannot drift again.
Also: the four client log attrs move to ClientInfo.LogAttrs(), which is now
their single definition and omits fields the client did not report rather than
persisting empty keys into opslog; startPlannedPlaybackV3 takes the resolved
identity instead of re-parsing the headers; client_label_full is omitted when it
would repeat client_label; getSessionClientLabelFull delegates to
getSessionClientLabel instead of re-implementing it; the Activity search matches
the exact label so a build number is findable; and the web ClientPlaybackContextV3
type mirrors the two new optional fields.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>