{"record":{"id":"426b89fb2d33806b","repo":"tinyhumansai/openhuman","slug":"discord-link-start-response-missing-required-strin-426b89","errorCode":null,"errorMessage":"Discord link start response missing required string field: instructions","messagePattern":"Discord link start response missing required string field: instructions","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/src/services/api/channelConnectionsApi.ts","lineNumber":82,"sourceCode":"  return unwrapped as T[];\n}\n\nfunction expectObject<T extends object>(payload: unknown, context: string): T {\n  const unwrapped = unwrapCliEnvelope<unknown>(payload);\n  const record = asRecord(unwrapped);\n  if (!record) {\n    throw new Error(`${context} returned an invalid response shape`);\n  }\n  return record as T;\n}\n\nfunction expectDiscordLinkStart(payload: unknown): DiscordLinkStartResult {\n  const record = expectObject<Record<string, unknown>>(payload, 'Discord link start');\n  if (typeof record.linkToken !== 'string' || !record.linkToken) {\n    throw new Error('Discord link start response missing required string field: linkToken');\n  }\n  if (typeof record.instructions !== 'string') {\n    throw new Error('Discord link start response missing required string field: instructions');\n  }\n  return { linkToken: record.linkToken, instructions: record.instructions };\n}\n\nfunction expectDiscordLinkComplete(payload: unknown): DiscordLinkCheckResult {\n  const record = expectObject<Record<string, unknown>>(payload, 'Discord link complete');\n  if (typeof record.linked !== 'boolean') {\n    throw new Error('Discord link complete response missing required boolean field: linked');\n  }\n  const details =\n    record.details !== undefined && record.details !== null\n      ? (record.details as Record<string, unknown>)\n      : null;\n  return { linked: record.linked, details };\n}\n\nfunction normalizeConnectResult(payload: unknown): ChannelConnectionResult {\n  const record = expectObject<Record<string, unknown>>(payload, 'Channel connect');","sourceCodeStart":64,"sourceCodeEnd":100,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/a221052e0df5b1f7598fceba7329fd1af95d6699/app/src/services/api/channelConnectionsApi.ts#L64-L100","documentation":"Thrown by expectDiscordLinkStart() when the 'openhuman.channels_discord_link_start' response object's 'instructions' field is missing or not a string (linkToken already passed its check). The managed Discord link flow needs the instructions text to show the user what to paste into Discord, so the frontend refuses a payload without it.","triggerScenarios":"discordLinkStart() resolves but the core returns {linkToken: '...'} with no instructions key, instructions: null / number / object, or a CLI envelope whose result carries only the token. Note: an empty string '' passes — only absence or a non-string type throws.","commonSituations":"Core version that mints the token but skips building instructions (e.g. bot-invite template missing or feature-flagged off); frontend/core skew after a partial update; a refactor of the Rust response struct dropping or renaming the field.","solutions":["Restart the core / relaunch the app so core and frontend versions match (same fix as the linkToken variant of this error)","Curl openhuman.channels_discord_link_start on /rpc and verify result.instructions is a string","Check core logs: the instructions string is usually composed from the bot config; a missing bot handle/config yields no instructions","If developing the handler, always set instructions (fall back to a static template string core-side rather than omitting the field)"],"exampleFix":"// Rust core: never omit the field\n// before\nif let Some(instructions) = build_instructions() { resp.insert(\"instructions\", instructions); }\n\n// after\nresp.insert(\"instructions\", build_instructions().unwrap_or_else(||\n    \"Send !start <token> to the OpenHuman bot in Discord.\".to_string()));","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"function hasInstructions(v: unknown): v is { instructions: string } {\n  const r = v as Record<string, unknown> | null | undefined;\n  const inner =\n    r && 'result' in r && 'logs' in r ? (r.result as Record<string, unknown>) : r;\n  return !!inner && typeof inner.instructions === 'string';\n}","tryCatchPattern":"try {\n  const r = await channelConnectionsApi.discordLinkStart();\n  renderInstructions(r.instructions);\n} catch (e) {\n  if (e instanceof Error && e.message.includes('instructions')) {\n    showFallbackInstructions(r_linkTokenIfAvailable); // static '!start <token>' copy\n  } else throw e;\n}","preventionTips":["Core-side: default instructions to a static template instead of omitting the field","Cover the instructions field in the same contract tests as linkToken","Treat any single-field miss in this payload as version skew and restart/resync builds"],"tags":["rpc","validation","discord","channels","contract-mismatch"],"backgroundTag":null,"analyzedSha":"a221052e0df5b1f7598fceba7329fd1af95d6699","analyzedAt":"2026-08-16T12:47:06.542Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}