{"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":"validation","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/a221052e0df5b1f7598fceba7329fd1af95d6699/app/src/services/api/threadUsageApi.ts#L52-L88","documentation":"Thrown by fetchThreadTokenUsage() when the 'openhuman.threads_token_usage' response's .data is falsy (null, undefined, or the whole response missing). The contract says a thread with no completed turns returns zeros with hasUsage: false — so an empty envelope means the core either predates the method's zero-fill behavior, returned null for an unknown thread_id, or wrapped nothing.","triggerScenarios":"Calling fetchThreadTokenUsage(threadId) for a thread id the core does not know (deleted, wrong workspace, or not yet persisted); an older core returning {data: null} instead of a zeroed usage record; a mock resolving {data: null}.","commonSituations":"Opening thread usage/cost UI immediately after creating a thread on an older core; pointing the app at a workspace whose threads db was reset; frontend/core version skew on the threads_token_usage controller.","solutions":["Verify the thread exists on the core side (threads list / the chat itself loads)","Update core and app together — newer cores return a zeroed record with hasUsage: false instead of null","If it persists, curl openhuman.threads_token_usage with the thread_id and inspect the raw result","As a client-side mitigation, catch this specific error and render zeros/hasUsage: false (usage display is non-critical)"],"exampleFix":"// before\nconst usage = await fetchThreadTokenUsage(threadId);\n\n// after\nlet usage: ThreadTokenUsage;\ntry {\n  usage = await fetchThreadTokenUsage(threadId);\n} catch (e) {\n  if (e instanceof Error && e.message.includes('empty envelope')) {\n    usage = zeroUsage(threadId); // display zeros, mark hasUsage: false\n  } else throw e;\n}","handlingStrategy":"fallback","validationCode":"// Skip the fetch for threads you know are brand-new if the UI can tolerate zeros:\nif (!threadHasCompletedTurns(threadId)) renderZeroUsage(); // avoids calling on older cores","typeGuard":"function isThreadUsageEnvelope(v: unknown): v is { data: { thread_id: string; input_tokens: number } } {\n  const r = v as Record<string, unknown> | null | undefined;\n  const d = r?.data as Record<string, unknown> | undefined;\n  return !!d && typeof d.thread_id === 'string' && typeof d.input_tokens === 'number';\n}","tryCatchPattern":"try {\n  const usage = await fetchThreadTokenUsage(threadId);\n  renderUsage(usage);\n} catch (e) {\n  if (e instanceof Error && e.message.includes('empty envelope')) {\n    renderUsage(zeroUsage(threadId)); // usage display degrades to zeros\n  } else throw e;\n}","preventionTips":["Update core and app together so threads_token_usage zero-fills instead of returning null","Treat usage/cost display as non-critical: always have a zero-usage fallback ready","Verify thread ids passed in actually exist on the current core workspace"],"tags":["rpc","validation","thread","usage","envelope"],"backgroundTag":null,"analyzedSha":"a221052e0df5b1f7598fceba7329fd1af95d6699","analyzedAt":"2026-08-16T12:47:06.542Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}