tinyhumansai/openhuman · warning
threads_token_usage returned an empty envelope
Error message
threads_token_usage returned an empty envelope
What it means
Thrown by fetchThreadTokenUsage() when the response to 'openhuman.threads_token_usage' has no `data` object (falsy). Per the function's contract, a thread with no completed turns still returns zeros with hasUsage:false — so an entirely empty envelope is abnormal: the core either does not implement the method's full response (older version) or the handler failed silently. The threadId is passed as thread_id; a wrong id typically still yields a zero-usage payload, not an empty envelope.
Solutions
- Restart/update the core so threads_token_usage returns the {data:{input_tokens,...}} envelope.
- Inspect the raw response once (log before the !d check) to see whether the payload is top-level or an error object.
- Verify via GET /schema that the running core registers threads_token_usage with the expected return shape.
- Treat as non-fatal in the UI: catch and render the usage panel as 'unavailable' rather than breaking the thread view.
Example fix
// before
const d = response?.data;
if (!d) throw new Error('threads_token_usage returned an empty envelope');
// after — degrade gracefully so the panel never blocks the thread
const d = response?.data;
if (!d) {
console.warn('threads_token_usage returned an empty envelope for', threadId);
return { threadId, inputTokens: 0, outputTokens: 0, costUsd: 0, hasUsage: false } satisfies ThreadTokenUsage;
} Defensive patterns
Strategy: fallback
Type guard
function isThreadUsageEnvelope(v: unknown): v is { data: ThreadTokenUsageWire } {
return Boolean(v) && typeof v === 'object'
&& Boolean((v as { data?: unknown }).data);
} Try / catch
try {
return await fetchThreadTokenUsage(threadId);
} catch (e) {
if (e instanceof Error && e.message === 'threads_token_usage returned an empty envelope') {
// usage is a non-critical panel — degrade, don't break the thread view
return zeroUsage(threadId, /* hasUsage */ false);
}
throw e;
} Prevention
- Treat token-usage as an optional enhancement: always render a zero/hasUsage:false fallback.
- Update core together with the UI so the usage envelope is populated.
- Log the raw response once when this fires to distinguish version skew from handler bugs.
When it happens
Trigger: UI running against a core built before threads_token_usage returned the ThreadTokenUsageWire payload; the RPC resolved through an error path that produced {error:...} without data; envelope shape changed (payload at top level, not under data).
Common situations: Version skew after updating only the app; external core via OPENHUMAN_CORE_REUSE_EXISTING=1; usage panel opened right after a core restart before stores are warm.
Related errors
- Discord link start response missing required string field…
- MISSING_ARTIFACT_PATH
- Model test RPC returned no result for
- provider_surfaces_list_queue: unexpected empty response
- RPC envelope contains undefined data
AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17).
Data as JSON: /api/errors/898fa1c0c29bafea.
Report an issue: GitHub.
Appendix: source
Thrown at app/src/services/api/threadUsageApi.ts:70
subagents?: ThreadSubagentUsageWire[];
}
interface Envelope<T> {
data?: T;
}
/**
* Fetch a thread's persisted token/cost totals from the core (read back from
* its session transcripts). Returns zeros with `hasUsage: false` for a thread
* that has no completed turns yet.
*/
export async function fetchThreadTokenUsage(threadId: string): Promise<ThreadTokenUsage> {
const response = await callCoreRpc<Envelope<ThreadTokenUsageWire>>({
method: 'openhuman.threads_token_usage',
params: { thread_id: threadId },
});
const d = response?.data;
if (!d) throw new Error('threads_token_usage returned an empty envelope');
return {
threadId: d.thread_id,
inputTokens: d.input_tokens,
outputTokens: d.output_tokens,
cachedInputTokens: d.cached_input_tokens,
costUsd: d.cost_usd,
turnCount: d.turn_count,
lastTurnInputTokens: d.last_turn_input_tokens,
lastTurnOutputTokens: d.last_turn_output_tokens,
contextWindow: d.context_window,
model: d.model,
updated: d.updated,
hasUsage: d.has_usage,
subagents: (d.subagents ?? []).map(s => ({
agentId: s.agent_id,
inputTokens: s.input_tokens,
outputTokens: s.output_tokens,
costUsd: s.cost_usd,View on GitHub (pinned to 7491200858)