tinyhumansai/openhuman · error
${context} returned an invalid response shape
Error message
${context} returned an invalid response shape What it means
expectArray unwrapped the payload (first stripping a CLI envelope { result, logs } if present) and the value was not an Array. The context string names the operation, e.g. 'Channel definitions', 'Channel status', 'Discord guild list', 'Discord channel list'. It marks a contract violation between the core/CLI and this client.
Source
Thrown at app/src/services/api/channelConnectionsApi.ts:62
function asRecord(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return null;
}
return value as Record<string, unknown>;
}
function unwrapCliEnvelope<T>(payload: unknown): T {
const record = asRecord(payload);
if (record && 'result' in record && 'logs' in record && Array.isArray(record.logs)) {
return record.result as T;
}
return payload as T;
}
function expectArray<T>(payload: unknown, context: string): T[] {
const unwrapped = unwrapCliEnvelope<unknown>(payload);
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');
}View on GitHub (pinned to a221052e0d)
Solutions
- Log the raw payload at the failure site and compare it with the core handler's actual return shape for the named operation
- Restart/update the core so its response shape matches what this client version expects
- If you control the mock/test server, return the unwrapped array (or { result: [...], logs: [] })
- If the core legitimately changed, update this client's unwrapping/expectation to the new contract
Example fix
// before (mock/test server route)
app.get('/rpc', () => jsonResponse({ channels: [/* ... */] }));
// after
app.get('/rpc', () => jsonResponse([/* ... */]));
// or a CLI-shaped envelope the helper understands:
app.get('/rpc', () => jsonResponse({ result: [/* ... */], logs: [] })); Defensive patterns
Strategy: type-guard
Validate before calling
const raw = await callCoreRpc<unknown>(params); const isUsableArray = Array.isArray(unwrapCliEnvelope(raw)); if (!isUsableArray) logPayloadForContractDiff(raw);
Type guard
const isArrayOf = <T>(v: unknown): v is T[] => Array.isArray(unwrapCliEnvelope(v));
function unwrapCliEnvelope<T>(payload: unknown): T {
const record = typeof payload === 'object' && payload !== null ? payload as Record<string, unknown> : null;
if (record && 'result' in record && 'logs' in record && Array.isArray(record.logs)) {
return record.result as T;
}
return payload as T;
} Try / catch
try { const defs = await channelConnectionsApi.listChannels(); }
catch (e) {
if (String((e as Error).message).includes('invalid response shape')) {
logPayloadAndCoreVersion(); showError('Channel data unavailable — restart the app and retry.');
} else throw e;
} Prevention
- Log the unwrapped payload whenever a shape error fires — the diff against the handler is the fix
- Keep core and client versions locked together in releases
- Make test mocks return the bare array or the { result, logs } envelope exactly
When it happens
Trigger: channelConnectionsApi.listChannels()/status()/listDiscordGuilds()/listDiscordChannels() receiving an object or string instead of an array — e.g. an error envelope { error: ... } passed through as success, a handler change returning { channels: [...] } instead of a bare array, or a legacy CLI path emitting a scalar.
Common situations: Version skew after a core-side response refactor; a CLI-in-the-middle returning its envelope in a shape unwrapCliEnvelope doesn't recognize ({ result, logs } with logs non-array bypasses the unwrap); mocks in tests returning wrapped objects; error payloads not surfaced as exceptions upstream.
Related errors
- Model test RPC returned no result for ${workload} via ${prov
- Discord link start response missing required string field: l
- Discord link start response missing required string field: i
- Discord link complete response missing required boolean fiel
- Channel connect response missing status
AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16).
Data as JSON: /api/errors/435d79271eef5125.
Report an issue: GitHub.