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

  1. Restart/update the core so threads_token_usage returns the {data:{input_tokens,...}} envelope.
  2. Inspect the raw response once (log before the !d check) to see whether the payload is top-level or an error object.
  3. Verify via GET /schema that the running core registers threads_token_usage with the expected return shape.
  4. 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

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


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)