tinyhumansai/openhuman · error
Discord link start response missing required string field: l
Error message
Discord link start response missing required string field: linkToken
What it means
Thrown by expectDiscordLinkStart() in app/src/services/api/channelConnectionsApi.ts after the 'openhuman.channels_discord_link_start' RPC has already resolved successfully. The frontend peels any CLI envelope ({result, logs}) and then requires 'linkToken' to be a non-empty string. It signals a frontend/core response-contract mismatch: the Rust core answered the method but its payload carries no usable link token.
Source
Thrown at app/src/services/api/channelConnectionsApi.ts:79
if (!Array.isArray(unwrapped)) {
throw new Error(`${context} returned an invalid response shape`);
}
return unwrapped as T[];
}
function expectObject<T extends object>(payload: unknown, context: string): T {
const unwrapped = unwrapCliEnvelope<unknown>(payload);
const record = asRecord(unwrapped);
if (!record) {
throw new Error(`${context} returned an invalid response shape`);
}
return record as T;
}
function expectDiscordLinkStart(payload: unknown): DiscordLinkStartResult {
const record = expectObject<Record<string, unknown>>(payload, 'Discord link start');
if (typeof record.linkToken !== 'string' || !record.linkToken) {
throw new Error('Discord link start response missing required string field: linkToken');
}
if (typeof record.instructions !== 'string') {
throw new Error('Discord link start response missing required string field: instructions');
}
return { linkToken: record.linkToken, instructions: record.instructions };
}
function expectDiscordLinkComplete(payload: unknown): DiscordLinkCheckResult {
const record = expectObject<Record<string, unknown>>(payload, 'Discord link complete');
if (typeof record.linked !== 'boolean') {
throw new Error('Discord link complete response missing required boolean field: linked');
}
const details =
record.details !== undefined && record.details !== null
? (record.details as Record<string, unknown>)
: null;
return { linked: record.linked, details };
}View on GitHub (pinned to a221052e0d)
Solutions
- Restart the core (Settings -> Restart Core) or relaunch the desktop app so the frontend and core binaries match; unset OPENHUMAN_CORE_REUSE_EXISTING if it pins an old core
- Inspect the raw response: POST the core /rpc endpoint with method openhuman.channels_discord_link_start and check whether result.linkToken exists and is non-empty
- Fetch GET /schema on the core port and confirm channels_discord_link_start is registered (a gated-out domain yields unknown-method instead, which fails earlier)
- Check core logs for link-token creation/persistence errors at request time
- If developing the Rust handler, return exactly { linkToken: <non-empty string>, instructions: <string> } with camelCase keys
Example fix
// before
const { linkToken, instructions } = await channelConnectionsApi.discordLinkStart();
// after
try {
const { linkToken, instructions } = await channelConnectionsApi.discordLinkStart();
showDiscordLinkInstructions(instructions, linkToken);
} catch (e) {
if (e instanceof Error && e.message.includes('linkToken')) {
surfaceNotice('Core/frontend version mismatch — restart the app, then retry the Discord link.');
} else {
throw e;
}
} Defensive patterns
Strategy: type-guard
Validate before calling
// Before launching the Discord flow, confirm the core registers the method:
const schema = await fetch(coreRpcBaseUrl + '/schema').then(r => r.json());
if (!JSON.stringify(schema).includes('channels_discord_link_start')) {
hideDiscordManagedLink(); // version skew — avoid the guaranteed throw
} Type guard
function isDiscordLinkStart(v: unknown): v is { linkToken: string; instructions: string } {
const r = v as Record<string, unknown> | null | undefined;
const inner =
r && 'result' in r && 'logs' in r && Array.isArray(r.logs)
? (r.result as Record<string, unknown>)
: r;
return (
!!inner &&
typeof inner.linkToken === 'string' &&
inner.linkToken.length > 0 &&
typeof inner.instructions === 'string'
);
} Try / catch
try {
const { linkToken, instructions } = await channelConnectionsApi.discordLinkStart();
showDiscordLinkInstructions(instructions, linkToken);
} catch (e) {
if (e instanceof Error && /linkToken|instructions/.test(e.message)) {
surfaceNotice('Core/frontend version mismatch — restart the app and retry.');
} else {
throw e;
}
} Prevention
- Update the desktop app and its embedded core together; never mix builds
- Avoid OPENHUMAN_CORE_REUSE_EXISTING against an older core binary in daily use
- Keep payload-shape contract tests (Vitest) asserting linkToken/instructions for the discord link RPC
When it happens
Trigger: Calling channelConnectionsApi.discordLinkStart() (Connections screen -> Discord managed-link flow) when the core returns {} or {instructions: '...'} without linkToken, a linkToken of a non-string type, an empty-string linkToken, or a CLI envelope whose inner result lacks the field.
Common situations: Frontend and Rust core versions out of sync (stale core pinned via OPENHUMAN_CORE_REUSE_EXISTING=1, or a half-updated desktop app); a core built without the channels feature returning a degraded handler shape; link-token minting failed core-side (persistence error) and the handler still returned an object.
Related errors
- Discord link start response missing required string field: i
- Discord link complete response missing required boolean fiel
- Channel connect response missing status
- ${context} returned an invalid response shape
- provider_surfaces_list_queue: unexpected empty response
AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16).
Data as JSON: /api/errors/301fde105afdcbcf.
Report an issue: GitHub.