{"record":{"id":"898fa1c0c29bafea","repo":"tinyhumansai/openhuman","slug":"threads-token-usage-returned-an-empty-envelope","errorCode":null,"errorMessage":"threads_token_usage returned an empty envelope","messagePattern":"threads_token_usage returned an empty envelope","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"app/src/services/api/threadUsageApi.ts","lineNumber":70,"sourceCode":"  subagents?: ThreadSubagentUsageWire[];\n}\n\ninterface Envelope<T> {\n  data?: T;\n}\n\n/**\n * Fetch a thread's persisted token/cost totals from the core (read back from\n * its session transcripts). Returns zeros with `hasUsage: false` for a thread\n * that has no completed turns yet.\n */\nexport async function fetchThreadTokenUsage(threadId: string): Promise<ThreadTokenUsage> {\n  const response = await callCoreRpc<Envelope<ThreadTokenUsageWire>>({\n    method: 'openhuman.threads_token_usage',\n    params: { thread_id: threadId },\n  });\n  const d = response?.data;\n  if (!d) throw new Error('threads_token_usage returned an empty envelope');\n  return {\n    threadId: d.thread_id,\n    inputTokens: d.input_tokens,\n    outputTokens: d.output_tokens,\n    cachedInputTokens: d.cached_input_tokens,\n    costUsd: d.cost_usd,\n    turnCount: d.turn_count,\n    lastTurnInputTokens: d.last_turn_input_tokens,\n    lastTurnOutputTokens: d.last_turn_output_tokens,\n    contextWindow: d.context_window,\n    model: d.model,\n    updated: d.updated,\n    hasUsage: d.has_usage,\n    subagents: (d.subagents ?? []).map(s => ({\n      agentId: s.agent_id,\n      inputTokens: s.input_tokens,\n      outputTokens: s.output_tokens,\n      costUsd: s.cost_usd,","sourceCodeStart":52,"sourceCodeEnd":88,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/app/src/services/api/threadUsageApi.ts#L52-L88","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\nconst d = response?.data;\nif (!d) throw new Error('threads_token_usage returned an empty envelope');\n\n// after — degrade gracefully so the panel never blocks the thread\nconst d = response?.data;\nif (!d) {\n  console.warn('threads_token_usage returned an empty envelope for', threadId);\n  return { threadId, inputTokens: 0, outputTokens: 0, costUsd: 0, hasUsage: false } satisfies ThreadTokenUsage;\n}","handlingStrategy":"fallback","validationCode":null,"typeGuard":"function isThreadUsageEnvelope(v: unknown): v is { data: ThreadTokenUsageWire } {\n  return Boolean(v) && typeof v === 'object'\n    && Boolean((v as { data?: unknown }).data);\n}","tryCatchPattern":"try {\n  return await fetchThreadTokenUsage(threadId);\n} catch (e) {\n  if (e instanceof Error && e.message === 'threads_token_usage returned an empty envelope') {\n    // usage is a non-critical panel — degrade, don't break the thread view\n    return zeroUsage(threadId, /* hasUsage */ false);\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["threads","token-usage","core-rpc","empty-response","version-skew"],"backgroundTag":"empty-rpc-response","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}