{"record":{"id":"f2c7d96a84379857","repo":"moeru-ai/airi","slug":"invalid-godot-stage-view-state-snapshot-payload","errorCode":null,"errorMessage":"Invalid Godot stage view-state snapshot payload.","messagePattern":"Invalid Godot stage view-state snapshot payload\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-shared/src/godot-stage/view-state.ts","lineNumber":143,"sourceCode":"  state: StageViewState\n  reason: StageViewSnapshotReason\n  /** Runtime-only avatar bounds. This is not persisted Godot view state. */\n  avatarBounds?: StageAvatarBoundsPayload\n  requestId?: string\n}\n\nexport const StageViewSnapshotPayloadSchema = strictObject({\n  state: StageViewStateSchema,\n  reason: picklist(['loaded', 'remote-patch', 'local-input', 'request', 'shutdown-flush']),\n  avatarBounds: optional(StageAvatarBoundsPayloadSchema),\n  requestId: requestIdSchema,\n})\n\n/** Parses a Godot-emitted view-state snapshot. */\nexport function parseStageViewSnapshotPayload(payload: unknown): StageViewSnapshotPayload {\n  const result = safeParse(StageViewSnapshotPayloadSchema, payload)\n  if (!result.success)\n    throw new Error('Invalid Godot stage view-state snapshot payload.')\n\n  return {\n    state: result.output.state,\n    reason: result.output.reason,\n    ...(result.output.avatarBounds ? { avatarBounds: result.output.avatarBounds } : {}),\n    ...(result.output.requestId != null ? { requestId: result.output.requestId } : {}),\n  }\n}\n\n/** Stable machine-readable Godot view-state error code. */\nexport type StageViewErrorCode\n  = | 'invalid-payload'\n    | 'invalid-state-file'\n    | 'persistence-failed'\n    | 'storage-root-missing'\n    | 'view-state-unavailable'\n\n/** Error event emitted by Godot for view-state request, validation, or lifecycle failures. */","sourceCodeStart":125,"sourceCodeEnd":161,"githubUrl":"https://github.com/moeru-ai/airi/blob/677329427f32468c74b17f3ec47eeca4e05bec65/packages/stage-shared/src/godot-stage/view-state.ts#L125-L161","documentation":"Thrown by parseStageViewSnapshotPayload when a Godot-emitted snapshot fails safeParse against StageViewSnapshotPayloadSchema. The schema requires `state` (full StageViewStateSchema), a `reason` that must be one of 'loaded' | 'remote-patch' | 'local-input' | 'request' | 'shutdown-flush', an optional `avatarBounds`, and a `requestId` — all under a strictObject that rejects unknown or missing keys. It protects the host from malformed snapshots coming over the Godot bridge.","triggerScenarios":"Godot emits a snapshot with a new `reason` value not in the picklist, omits `state` or sends a state with an out-of-range camera field, includes an extra key, or the payload is double-stringified JSON.","commonSituations":"Updating the Godot engine side (engines/stage-tamagotchi-godot) without rebuilding packages/stage-shared, so the two sides disagree on the reason enum or state shape; a Godot script error producing a half-initialized state table; string/number type drift for camera fields between GDScript and TypeScript.","solutions":["Log safeParse issues (r.issues includes the exact path, e.g. ['reason']) before or instead of relying on the wrapped message","Compare the emitted reason strings in the Godot source with the picklist in view-state.ts and rebuild/align both sides","Add a regression Vitest case that feeds the real captured payload (from a log) through StageViewSnapshotPayloadSchema","If a new reason is legitimately added on the Godot side, extend the picklist and StageViewSnapshotReason in the same change"],"exampleFix":"// before\nconst snap = parseStageViewSnapshotPayload(event.data) // generic failure\n\n// after\nimport { safeParse, StageViewSnapshotPayloadSchema } from '../godot-stage/view-state'\nconst r = safeParse(StageViewSnapshotPayloadSchema, event.data)\nif (!r.success) {\n  console.error('snapshot issues', r.issues) // shows e.g. reason: invalid option\n  return\n}\nconst snap = parseStageViewSnapshotPayload(event.data)","handlingStrategy":"validation","validationCode":"import { safeParse, StageViewSnapshotPayloadSchema } from '../godot-stage/view-state'\n\nconst checked = safeParse(StageViewSnapshotPayloadSchema, event.data)\nif (!checked.success) {\n  log.error('snapshot rejected', checked.issues)\n  return\n}\nconst snap = parseStageViewSnapshotPayload(event.data)","typeGuard":"const isStageViewSnapshotPayload = (p: unknown): boolean => safeParse(StageViewSnapshotPayloadSchema, p).success","tryCatchPattern":"catch (e) { if (e.message.includes('view-state snapshot payload')) { requestFreshSnapshot(); return } throw e }","preventionTips":["Extend the reason picklist in the same commit that adds a new Godot reason","Never forward snapshot JSON as a string; parse at the transport boundary","Pin the Godot build and stage-shared version in CI so schema drift fails loudly"],"tags":["valibot","schema-validation","godot","snapshot","ipc-payload"],"backgroundTag":"schema-validation-failed","analyzedSha":"677329427f32468c74b17f3ec47eeca4e05bec65","analyzedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}