Usage accounting without a false ledger
Live thread context, historical transcript accounting, and provider-reported subscription limits answer different questions and keep separate evidence, merge, pricing, and refresh rules.
What this chapter resolves
- Trace the live context-window snapshot from provider adapter to the web composer.
- Trace historical Claude and Codex transcript records through parsing, deduplication, bucketing, pricing, and environment merge.
- Explain Grok transcript accounting, custom prices, pooled subscription windows, hubs, and reset credits.
- Interpret coverage, source fingerprints, cache state, and cost provenance without claiming a billing settlement.
- Keep task and subagent accounting as a distinct later concern.
“Usage” names three different questions in T3 Code.
The composer asks: how full is this thread’s context window at the latest valid provider update? The Usage history asks: what token-shaped records can this environment read from provider transcript directories over a historical time range? The Limits view asks: how much provider-reported subscription capacity remains across the selected accounts and environments? They share words such as model, tokens, account, and session, but they do not share a source of truth, an update cadence, or a settlement guarantee.
The live lane projects a current context snapshot
The runtime contract calls the canonical event
thread.token-usage.updated. Its snapshot requires usedTokens; it may also carry
the provider’s total processed tokens, maximum context size, input/cache/output
breakdown, reasoning subset, duration, tool count, and a compaction flag. The event
also carries provider, thread, time, and optional turn and provider-instance
identity.
const ThreadMetadataUpdatedPayload = Schema.Struct({
name: Schema.optional(TrimmedNonEmptyStringSchema),
metadata: Schema.optional(UnknownRecordSchema),
});
export type ThreadMetadataUpdatedPayload = typeof ThreadMetadataUpdatedPayload.Type;
export const ThreadTokenUsageSnapshot = Schema.Struct({
usedTokens: NonNegativeInt,
totalProcessedTokens: Schema.optional(NonNegativeInt),
maxTokens: Schema.optional(PositiveInt),
inputTokens: Schema.optional(NonNegativeInt),
cachedInputTokens: Schema.optional(NonNegativeInt),
outputTokens: Schema.optional(NonNegativeInt),
reasoningOutputTokens: Schema.optional(NonNegativeInt),
lastUsedTokens: Schema.optional(NonNegativeInt),
lastInputTokens: Schema.optional(NonNegativeInt),
lastCachedInputTokens: Schema.optional(NonNegativeInt),
lastOutputTokens: Schema.optional(NonNegativeInt),
lastReasoningOutputTokens: Schema.optional(NonNegativeInt),
toolUses: Schema.optional(NonNegativeInt),
durationMs: Schema.optional(NonNegativeInt),
compactsAutomatically: Schema.optional(Schema.Boolean),
autoCompactThreshold: Schema.optional(PositiveInt),Source inspection finds canonical token-usage emission in two adapters:
| Adapter | Provider-side input | Canonical emission boundary | Coverage qualification |
|---|---|---|---|
| Codex | native thread/tokenUsage/updated notification | CodexAdapter normalizes it, rejecting a non-positive usedTokens | emitted when that native notification arrives; total processed is read from native total, current use from native last |
| Claude | normalized Claude SDK message / task-progress usage | emitThreadTokenUsage | emitted only when normalization produced usage; the adapter remembers its last known values |
| Cursor, Grok, OpenCode, ACP adapters | no canonical emission branch found in this revision | — | absence in this audit is not evidence that a provider can never expose usage; it means this build does not project it through this event |
method,
detail: error.message,
cause: error,
});
}
type CodexLifecycleItem =
| EffectCodexSchema.V2ItemStartedNotification["item"]
| EffectCodexSchema.V2ItemCompletedNotification["item"];
type CodexToolUserInputQuestion =
| EffectCodexSchema.ServerRequest__ToolRequestUserInputQuestion
| EffectCodexSchema.ToolRequestUserInputParams__ToolRequestUserInputQuestion;
const ApprovalDecisionPayload = Schema.Struct({
decision: ProviderApprovalDecision,
});
function readPayload<A>(
schema: Schema.Schema<A>,
payload: ProviderEvent["payload"],
): A | undefined {
const isPayload = Schema.is(schema);
return isPayload(payload) ? payload : undefined;
}
function trimText(value: string | undefined | null): string | undefined {
const trimmed = value?.trim();
return trimmed && trimmed.length > 0 ? trimmed : undefined;
}
function asUnknownRecord(value: unknown): Record<string, unknown> | undefined { readPayload(EffectCodexSchema.V2ItemCompletedNotification, event.payload);
const item = payload?.item;
if (!item) {
return undefined;
}
const itemType = toCanonicalItemType(item.type);
if (itemType === "unknown" && lifecycle !== "item.updated") {
return undefined;
}
const detail = itemDetail(itemType, item);
const toolPresentation = item.type === "mcpToolCall" ? mcpToolPresentation(item) : {};
const title = itemTitle(itemType, item, toolPresentation);
const status =
lifecycle === "item.started"
? "inProgress"
: lifecycle === "item.completed"
? "status" in item && (item.status === "failed" || item.status === "declined")
? item.status resumeCursor,
updatedAt: yield* nowIso,
};
});
const ensureAssistantTextBlock = Effect.fn("ensureAssistantTextBlock")(function* (
context: ClaudeSessionContext,
blockIndex: number,
options?: {
readonly fallbackText?: string;
readonly streamClosed?: boolean;
},
) {
const turnState = context.turnState;
if (!turnState) {
return undefined;
}
const existing = turnState.assistantTextBlocks.get(blockIndex);
if (existing && !existing.completionEmitted) {
if (existing.fallbackText.length === 0 && options?.fallbackText) {
existing.fallbackText = options.fallbackText;
}
if (options?.streamClosed) {
existing.streamClosed = true;
}
return { blockIndex, block: existing };
}
const block: AssistantTextBlockState = {
itemId: yield* randomUUIDv4,
blockIndex,
emittedTextDelta: false,
fallbackText: options?.fallbackText ?? "",
streamClosed: options?.streamClosed ?? false,
completionEmitted: false,
};
turnState.assistantTextBlocks.set(blockIndex, block);The provider service validates the provider/instance association, optionally writes
the canonical event to its best-effort NDJSON log, and publishes it on the hot
runtime stream. Runtime ingestion accepts a token-usage event
only when usedTokens > 0, then writes one context-window.updated activity with
the event id as activity id. The web selector walks activities from newest to oldest
and takes the first valid context-window payload. It derives percentage and
remaining capacity from usedTokens / maxTokens only when a usable maximum exists.
It does not sum snapshots, recover skipped notifications, or consult transcript
files.
},
];
}
case "thread.token-usage.updated": {
const payload = buildContextWindowActivityPayload(event);
if (!payload) {
return [];
}
return [
{
id: event.eventId,
createdAt: event.createdAt,
tone: "info",
kind: "context-window.updated",
summary: "Context window updated",
payload,
turnId: toTurnId(event.turnId) ?? null, return {
usedTokens,
totalProcessedTokens: asFiniteNumber(payload?.totalProcessedTokens),
maxTokens,
remainingTokens,
usedPercentage,
remainingPercentage,
inputTokens: asFiniteNumber(payload?.inputTokens),
cachedInputTokens: asFiniteNumber(payload?.cachedInputTokens),
outputTokens: asFiniteNumber(payload?.outputTokens),
reasoningOutputTokens: asFiniteNumber(payload?.reasoningOutputTokens),
lastUsedTokens: asFiniteNumber(payload?.lastUsedTokens),
lastInputTokens: asFiniteNumber(payload?.lastInputTokens),
lastCachedInputTokens: asFiniteNumber(payload?.lastCachedInputTokens),
lastOutputTokens: asFiniteNumber(payload?.lastOutputTokens),
lastReasoningOutputTokens: asFiniteNumber(payload?.lastReasoningOutputTokens),
toolUses: asFiniteNumber(payload?.toolUses),
durationMs: asFiniteNumber(payload?.durationMs),
compactsAutomatically: asBoolean(payload?.compactsAutomatically) ?? false,
autoCompactThreshold: asFiniteNumber(payload?.autoCompactThreshold),
updatedAt: activity.createdAt,
};
}
return null;
}
export function formatContextWindowTokens(value: number | null): string {
if (value === null || !Number.isFinite(value)) {
return "0";
}
if (value < 1_000) {
return `${Math.round(value)}`;
}
if (value < 10_000) {
return `${(value / 1_000).toFixed(1).replace(/\.0$/, "")}k`;
}
if (value < 1_000_000) {
return `${Math.round(value / 1_000)}k`;
}
return `${(value / 1_000_000).toFixed(1).replace(/\.0$/, "")}m`;The historical lane scans three provider homes, not orchestration state
The version-5 usage contract reads selected CLI homes: Claude JSONL
under a resolved Claude home (preferring .claude/projects, with a projects
fallback), Codex JSONL under the resolved Codex shared-home sessions tree, and
Grok updates.jsonl files under its resolved sessions home. The
server exposes preaggregated buckets, never raw transcript records. Consequently it
can include provider turns made outside T3 Code, but it cannot account for a
provider whose history is not in those three scanned layouts.
/**
* Usage reporting contract.
*
* Each environment scans the provider CLIs' own on-disk session transcripts
* (`~/.claude/projects/**\/*.jsonl`, `~/.codex/sessions/**\/*.jsonl`,
* `~/.grok/sessions/**\/updates.jsonl`) rather than relying on T3 Code's own
* orchestration projections, so usage stays complete even for turns that were
* never driven through T3 Code. This mirrors the approach `ccusage` takes.
*
* Environments return pre-aggregated `(day, hourStart?, provider, model)`
* buckets. Raw transcript records never cross the wire.
*
* @module usage
*/
import * as Schema from "effect/Schema";
import { NonNegativeInt, TrimmedNonEmptyString } from "./baseSchemas.ts";
/**
* Bumped whenever the shape of {@link UsageSummary} changes incompatibly. The
* client renders partial coverage when an environment reports an older version
* rather than failing the whole page.
*/
export const USAGE_CONTRACT_VERSION = 5 as const;
/**
* Oldest {@link UsageSummary} version a current client will still merge.
*
* v5 only adds `grok` to {@link UsageProviderKind}; v4 Claude/Codex buckets
* remain valid, so mixed-version environments keep those totals instead of
* treating every older server as stale.
*/
export const USAGE_MERGE_COMPATIBLE_SINCE = 4 as const;
export const UsageProviderKind = Schema.Literals(["claude", "codex", "grok"]);
export type UsageProviderKind = typeof UsageProviderKind.Type;
/**
* A calendar day in the reporting time zone, formatted `YYYY-MM-DD`.
*
* Days are bucketed server-side so that a turn always lands on the day the user
* experienced it, not the UTC day.
*/
const USAGE_DAY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
export const UsageDay = TrimmedNonEmptyString.check(Schema.isPattern(USAGE_DAY_PATTERN)).pipe(
Schema.brand("UsageDay"),
);
export type UsageDay = typeof UsageDay.Type;
export const UsageResolution = Schema.Literals(["day", "hour"]);
export type UsageResolution = typeof UsageResolution.Type;
/**
* Why a bucket's cost is what it is.
*
* - `providerReported` - the transcript carried an explicit cost figure.
* - `modelPriced` - we used a custom price override or the LiteLLM rate table.
* - `unpriced` - tokens are known, rates are not. Counted in totals, excluded
* from cost.
*/
export const UsageCostSource = Schema.Literals(["providerReported", "modelPriced", "unpriced"]);
export type UsageCostSource = typeof UsageCostSource.Type;
/**
* Token counts for a bucket.
*
* `cachedInputTokens` and `cacheCreationTokens` are disjoint from
* `uncachedInputTokens`; summing all three gives total input. `reasoningTokens`
* is a *subset* of `outputTokens` (Codex reports it that way, and Anthropic
* folds thinking into output), so it must never be added on top.
*/
export const UsageTokenTotals = Schema.Struct({
uncachedInputTokens: NonNegativeInt,
cachedInputTokens: NonNegativeInt,
cacheCreationTokens: NonNegativeInt,
outputTokens: NonNegativeInt,
reasoningTokens: NonNegativeInt,
});
export type UsageTokenTotals = typeof UsageTokenTotals.Type;
/**
* One `(day, hourStart?, provider, model)` cell. `hourStart` is the UTC start
* instant of a rolling bucket and is present only for hourly requests.
*
* `costUsd` is the raw API-equivalent cost of these tokens. It is not money
* spent: subscription plans bill separately. `unpricedRecords` counts records
* whose tokens are included in the token totals but which contributed nothing
* to `costUsd`.
*/
export const UsageBucket = Schema.Struct({
day: UsageDay,
hourStart: Schema.optional(TrimmedNonEmptyString),
provider: UsageProviderKind,
model: TrimmedNonEmptyString,
totals: UsageTokenTotals,
costUsd: Schema.Number,
/**
* What the cached input would have cost at full input rates minus what it
* actually cost. Requires the rate table, so it is computed alongside cost
* rather than derived on the client. }
}
const fetched = yield* httpClient.get(LITELLM_RATES_URL).pipe(
Effect.flatMap(HttpClientResponse.filterStatusOk),
Effect.flatMap((response) => response.json),
Effect.timeout(10_000),
Effect.catchCause(() => Effect.succeed(null)),
);
if (fetched === null) {
// The refresh failed; whatever we are serving is now past its TTL and
// must not keep claiming to be fresh.
if (rates.size > 0) ratesStatus = "cached";
return;
}
const parsed = parseRateTable(fetched);
if (parsed.size === 0) return;
rates = parsed;
ratesFetchedAtMs = now;
ratesStatus = "fresh";
yield* encodeRatesCache({ fetchedAtMs: now, document: fetched }).pipe(
Effect.flatMap((serialized) => fileSystem.writeFileString(ratesCachePath, serialized)),
Effect.catchCause(() => Effect.void),
);
});
const ensureRates = (force: boolean) => ratesLock.withPermit(loadRates(force));
const refreshRates = ensureRates(true).pipe(
Effect.map(pricing),
Effect.withSpan("UsageService.refreshRates"),
);
/**
* Claude's config dir is the home itself when overridden, but a default
* install nests transcripts under `~/.claude/projects`. Probe both.The time rule has two layers. Daily buckets use the caller-selected IANA time zone so a day reflects the user’s experience. Hourly queries have exact inclusive/ exclusive instants and are capped at 24 hours. Candidate files are screened using a UTC window with a 36-hour mtime slack, then individual records are parsed and tested against the actual requested range. A file’s mtime is therefore an optimization filter, not the semantic timestamp of a usage record.
Parser rules are provider-specific. Claude accepts assistant records with a usage
object and deduplicates repeated content-block accounting by message/request key.
Codex follows turn-context model state and last_token_usage deltas; it subtracts
cached tokens from the reported input count and applies a one-second fork-copy
suppression heuristic. Grok reads completed-turn updates, can split model-level
usage, converts provider cost ticks, and uses session, prompt, and model identity
for deduplication when available. Codex records carry no global duplicate key: consecutive
delta suppression and fork-copy suppression are parser-local. That one-second
threshold is implementation behavior, not a portable provider promise.
* headless `total_cost_usd_ticks`. Convert to dollars for pricing.
*/
export const GROK_COST_USD_TICKS_PER_DOLLAR = 10_000_000_000;
function grokCostTicksToUsd(ticks: unknown): number | null {
if (typeof ticks !== "number" || !Number.isFinite(ticks) || ticks < 0) return null;
return ticks / GROK_COST_USD_TICKS_PER_DOLLAR;
}
/* -------------------------------------------------------------------------- */
/* Claude Code */
/* -------------------------------------------------------------------------- */
/**
* Parses one line of a Claude Code transcript.
*
* T3 Code writes one record per assistant *content block*, and every one of
* those records repeats the same complete `usage` object for the parent
* message. Summing them overcounts by roughly 2.4x on a real workload, so the
* caller must drop repeats by `dedupeKey` and keep the first.
*/
export function parseClaudeLine(line: string): UsageRecord | null {
let parsed: unknown;
try {
parsed = JSON.parse(line);
} catch {
return null;
}
if (typeof parsed !== "object" || parsed === null) return null;
const record = parsed as Record<string, unknown>;
if (record["type"] !== "assistant") return null;
const message = record["message"];
if (typeof message !== "object" || message === null) return null;
const messageRecord = message as Record<string, unknown>;
const usage = messageRecord["usage"];
if (typeof usage !== "object" || usage === null) return null;
const usageRecord = usage as Record<string, unknown>;
const timestampMs = parseTimestampMs(record["timestamp"]);
if (timestampMs === null) return null;
const model = typeof messageRecord["model"] === "string" ? messageRecord["model"] : "";
if (model.length === 0) return null;
const messageId = typeof messageRecord["id"] === "string" ? messageRecord["id"] : null;
const requestId = typeof record["requestId"] === "string" ? record["requestId"] : null;
// Matches ccusage: prefer the message/request pair, fall back to whichever
// half exists. Records with neither cannot be de-duplicated.
const dedupeKey =
messageId === null && requestId === null ? null : `${messageId ?? ""}:${requestId ?? ""}`;
const cost = record["costUSD"];
return {
provider: "claude",
timestampMs,
model,Deduplicate before bucketing, then price with provenance
The reader streams JSONL files and uses a cache only when path, provider, size, and mtime still match. A cache miss or unreadable file does not change the semantic deduplication order: after provider-local parser suppression, aggregation removes non-null duplicate keys within a file, then removes matching Claude copy/fork keys across files, then applies time bounds and buckets by day (and optional hour), provider, and model. Totals treat reasoning as a subset of output, not an additional token category.
/**
* Folds one record in. Returns whether it actually contributed, so callers
* can derive per-window facts (distinct sessions, for one) from the records
* that landed rather than everything the mtime prefilter happened to admit.
*/
add(record: UsageRecord): boolean {
if (record.dedupeKey !== null) {
if (this.#seen.has(record.dedupeKey)) {
this.#duplicatesDropped += 1;
return false;
}
this.#seen.add(record.dedupeKey);
}
if (
this.#hourlyWindow !== null &&
(record.timestampMs < this.#hourlyWindow.sinceTimeMs ||
record.timestampMs >= this.#hourlyWindow.untilTimeMs)
) {
this.#outOfWindow += 1;
return false;
}
const day = this.#toDay(record.timestampMs);
if (
this.#hourlyWindow === null &&
(day < this.#options.sinceDay || day > this.#options.untilDay)
) {
this.#outOfWindow += 1;
return false;
}
const hourStart =
this.#hourlyWindow === null
? ""
: new Date(
this.#hourlyWindow.sinceTimeMs +
Math.floor((record.timestampMs - this.#hourlyWindow.sinceTimeMs) / HOUR_MS) * HOUR_MS,
).toISOString();
const key = `${day}\u0000${hourStart}\u0000${record.provider}\u0000${record.model}`;
let bucket = this.#buckets.get(key);
if (bucket === undefined) {
bucket = {
totals: EMPTY_TOTALS,
costUsd: 0,
cacheSavingsUsd: 0,
records: 0,
unpricedRecords: 0,
providerReportedRecords: 0,
sessions: new Set<string>(),
};
this.#buckets.set(key, bucket);
}
const priced = priceUsage(
this.#options.rates,
record.model,
record.totals,
record.reportedCostUsd,
this.#options.priceOverrides,
);
bucket.totals = addTotals(bucket.totals, record.totals);
bucket.costUsd += priced.costUsd;
bucket.cacheSavingsUsd += cacheSavingsUsd(
this.#options.rates,
record.model,
record.totals,
this.#options.priceOverrides,When a transcript contains a finite provider-reported USD value, that value wins. Otherwise the server can multiply uncached input, cache reads, cache creation, and output by a LiteLLM rate table. The cache is memory-then-disk, has a 24-hour TTL, and falls back to a still-cached table after a rate-fetch failure; without a usable table, a model is unpriced. The UI therefore labels the computed value as an API estimate. It is not evidence of subscription charges, discounts, enterprise contracts, or a provider invoice.
const key = normalizeRateKey(name);
if (key.length === 0) continue;
table.set(key, {
inputCostPerToken: input,
outputCostPerToken: output,
// Anthropic bills cache reads at a discount and cache writes at a
// premium. When a model omits them, cached input is priced as plain
// input rather than as free.
cacheReadCostPerToken: finiteNumber(entry.cache_read_input_token_cost) ?? input,
cacheCreationCostPerToken: finiteNumber(entry.cache_creation_input_token_cost) ?? input,
});
}
// `null` marks a bare name claimed at conflicting rates: no alias for it.
const aliasCandidates = new Map<string, ModelRate | null>();
for (const [key, rate] of table) {
const alias = bareModelName(key);
if (alias.length === 0 || alias === key || table.has(alias)) continue;
const held = aliasCandidates.get(alias);
if (held === undefined) {
aliasCandidates.set(alias, rate);
} else if (held !== null && !sameRate(held, rate)) {
aliasCandidates.set(alias, null);
}
}
for (const [alias, rate] of aliasCandidates) {
if (rate !== null) table.set(alias, rate);
}
return table;
}
function sameRate(a: ModelRate, b: ModelRate): boolean {
return (
a.inputCostPerToken === b.inputCostPerToken &&
a.outputCostPerToken === b.outputCostPerToken &&
a.cacheReadCostPerToken === b.cacheReadCostPerToken &&
a.cacheCreationCostPerToken === b.cacheCreationCostPerToken
);
}
function normalizeRateKey(model: string): string {
return model.trim().toLowerCase();
}
function bareModelName(key: string): string {
const slash = key.lastIndexOf("/");
return slash === -1 ? key : key.slice(slash + 1);
}
/**
* Drops a bracketed variant suffix such as `claude-fable-5-1[1m]`, which
* Claude Code writes for the 1M context tier. The rate table only knows the
* base name, and we price at the base tier anyway.
*/
function stripVariantSuffix(key: string): string {
const bracket = key.indexOf("[");
return bracket === -1 ? key : key.slice(0, bracket);
}
/**
* Models we never price, regardless of the table.
*
* `<synthetic>` marks locally generated messages that were never billed. BareCoverage and merge are part of the result
Each environment returns source diagnostics and a fingerprint containing host id, provider, resolved home path, and filesystem volume id. The web and mobile clients request every connected environment, reject an incompatible contract version, and merge only one claimant for an identical non-missing source fingerprint. They sum distinct source sessions rather than adding bucket session counts, which could count one session repeatedly across day/model buckets.
Custom prices are environment settings
Automatic model pricing is a convenience, not an authority. Web and desktop let a user set exact per-million input, output, cache-read, and cache-write rates for a model on one or several environments. Blank cache rates inherit the input rate; zero means free. Saving is fan-out work: each destination reports success, and a failed destination can be retried without rewriting the environments that already accepted the change. Closing the dialog does not create an offline write queue.
The historical scan applies these overrides before automatic pricing. A client that selects several environments can therefore see a mixed value when their local price settings differ. This is an intentional consequence of environment ownership.
Subscription limits are provider and account state
Provider snapshots can carry normalized rolling windows with stable ids, a kind, label, used percentage, optional reset time, and optional duration. Sparse updates merge by window id so one rate-limit event does not erase unrelated windows. An unavailable reason distinguishes an account that cannot report limits from a probe that failed while a prior good snapshot may still be useful.
The Limits view pools equivalent accounts across selected environments and optional CLIProxyAPI hubs. It presents remaining capacity, keeps the same account segment in the same visual column across windows, and explains the next reset. Codex accounts may also report banked reset credits; redemption pins the request to the displayed credit id so a retry cannot silently consume a different credit.
/usage-limits reads the same snapshot inside a thread. It does not run an agent or
refresh provider state. API-key accounts and some proxy configurations may never
report subscription windows, which is different from a temporary probe failure.
Zoom with the controls, +/−, or Ctrl/⌘ + trackpad scroll. Enable Pan to drag, use two-finger scrolling, or use the arrow keys. 0 fits the diagram; Esc leaves Pan or expanded view.
projectsListEntries: "projects.listEntries",
projectsReadFile: "projects.readFile",
projectsSearchContents: "projects.searchContents",
projectsSearchEntries: "projects.searchEntries",
projectsWriteFile: "projects.writeFile",
// Shell methods
shellOpenInEditor: "shell.openInEditor",
// Filesystem methods
filesystemBrowse: "filesystem.browse",
agentSessionsScan: "agentSessions.scan",
agentSessionsImport: "agentSessions.import",
assetsCreateUrl: "assets.createUrl",
attachmentsCreateUploadUrl: "attachments.createUploadUrl",
attachmentsDelete: "attachments.delete",
// Provider methods
providerUploadFeedback: "provider.uploadFeedback",
providerAuthStart: "provider.auth.start",
providerConsumeResetCredit: "provider.consumeResetCredit",
providerAuthComplete: "provider.auth.complete",
providerAuthCancel: "provider.auth.cancel",
providerAuthLogout: "provider.auth.logout",
providerAuthSubscribe: "provider.auth.subscribe",
/**
* Decides which environment owns each physical transcript directory.
*
* Several environments on one machine (worktree servers, for instance) resolve
* the same provider home and would otherwise double count every token. The
* first environment in a stable order claims a fingerprint; the rest have that
* provider's buckets dropped. Environments are sorted by id so the winner does
* not change between renders.
*/
function claimSources(environments: readonly EnvironmentUsage[]): {
readonly ownerByFingerprint: ReadonlyMap<string, EnvironmentId>;
readonly duplicates: readonly string[];
} {
const ownerByFingerprint = new Map<string, EnvironmentId>();
const duplicates: string[] = [];
const ordered = [...environments].sort((a, b) => a.environmentId.localeCompare(b.environmentId));
for (const environment of ordered) {
for (const source of environment.summary.sources) {
if (source.status === "missing") continue;
const key = fingerprintKey(source.fingerprint);
if (ownerByFingerprint.has(key)) {
duplicates.push(`${environment.label}: ${source.fingerprint.resolvedHomePath}`);
continue;
}
ownerByFingerprint.set(key, environment.environmentId);
}
}
return { ownerByFingerprint, duplicates };
}
/** Sources this environment owns after fingerprint claims, plus their buckets. */
function ownedContribution(
environment: EnvironmentUsage,
ownerByFingerprint: ReadonlyMap<string, EnvironmentId>,
): {
readonly buckets: readonly UsageBucket[];
readonly sessionsByProvider: ReadonlyMap<UsageProviderKind, number>;
} {
const ownedProviders = new Set<UsageProviderKind>();
const sessionsByProvider = new Map<UsageProviderKind, number>();
for (const source of environment.summary.sources) {
if (source.status === "missing") continue;
const key = fingerprintKey(source.fingerprint);
if (ownerByFingerprint.get(key) === environment.environmentId) {
const provider = source.fingerprint.provider;
ownedProviders.add(provider);
// Distinct within a directory. Summing per-bucket session counts instead
// would count a session once per day and model it spans.
sessionsByProvider.set(
provider,
(sessionsByProvider.get(provider) ?? 0) + source.distinctSessions,
);
}
}
return {
buckets: environment.summary.buckets.filter((bucket) => ownedProviders.has(bucket.provider)),
sessionsByProvider,
};Zoom with the controls, +/−, or Ctrl/⌘ + trackpad scroll. Enable Pan to drag, use two-finger scrolling, or use the arrow keys. 0 fits the diagram; Esc leaves Pan or expanded view.
Text equivalent
The left lane starts with a provider event, validates a positive used-token snapshot, persists a context-window activity, selects the newest valid activity for one thread, and renders a context meter. The right lane starts with Claude, Codex, and Grok transcript directories, streams JSONL records, parses provider-specific usage, removes duplicates, applies time bounds and buckets, obtains provider-reported or configured/rate-table cost provenance, and merges environment summaries by a host/provider/home/volume fingerprint. There is no arrow from the context meter into the historical total.
packages/contracts/src/providerRuntime.ts:309–331 ↗apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts:766–784 ↗apps/web/src/lib/contextWindow.ts:50–90 ↗packages/contracts/src/usage.ts:1–101 ↗apps/server/src/usage/usageAggregation.ts:109–178 ↗apps/server/src/usage/usagePricing.ts:85–148 ↗packages/contracts/src/rpc.ts:260–284 ↗packages/shared/src/usageMerge.ts:102–163 ↗There is an important diagnostic limitation. The contract
permits ok, missing, partial, and failed source states, but the service path
observed here emits missing when the directory is absent—or when the existence
check itself fails—and otherwise reports ok. Directory listing errors are swallowed
by the reader; unreadable files
increment skipped-file work but return no records; malformed-record count is
initialized to zero. An apparently ok zero is therefore not a proof that every
eligible record was readable. Treat coverage indicators as useful evidence, not as
completeness certificates.
* Errors on individual entries are swallowed: session files rotate and get
* removed while the walk is in flight, and a partial listing is far better than
* failing the page.
*
* `fileName` restricts the walk to a single basename (Grok's `updates.jsonl`).
* Grok sessions also ship multi-megabyte `chat_history` and `events` logs that
* never carry usage, so the basename filter keeps a cold scan off those files.
*/
export async function listTranscriptFiles(
root: string,
sinceMs: number,
options?: { readonly fileName?: string },
): Promise<readonly TranscriptFile[]> {
const found: TranscriptFile[] = [];
const fileName = options?.fileName;
const walk = async (dir: string): Promise<void> => {
let entries;
try {
entries = await NodeFSP.readdir(dir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
const child = NodePath.join(dir, entry.name);
if (entry.isDirectory()) {
await walk(child);
continue;
}
if (fileName !== undefined) {
if (entry.name !== fileName) continue;
} else if (!entry.name.endsWith(".jsonl")) {
continue;
}
try {
const stats = await NodeFSP.stat(child);
if (stats.mtimeMs >= sinceMs) {
found.push({ path: child, size: stats.size, mtimeMs: stats.mtimeMs });
}
} catch {
// Vanished between readdir and stat.
}
}
};
await walk(root);
return found;
} // resumed parse dedupes exactly like a full one.
const base = parsed.resumed && cached !== undefined ? cached.records : [];
const seen = new Set<string>();
const records = dedupeWithinFile([...base, ...parsed.records], seen);
const tailRecords = dedupeWithinFile(parsed.tailRecords, seen);
fileCache.set(filePath, {
size,
mtimeMs,
provider,
records,
tailRecords,
position: parsed.position,
});
cacheDirty = true;
return tailRecords.length === 0 ? records : [...records, ...tailRecords];
});
/** One provider directory's walk and parse, before rates are involved. */
interface ScannedDir {
readonly provider: UsageProviderKind;
readonly dir: string;
readonly volumeId: string;
/** Parsed records per file, or `null` when the directory does not exist. */
readonly files:
| readonly { readonly path: string; readonly records: readonly UsageRecord[] }[]
| null;
}
const collectDirs = Effect.fn("UsageService.collectDirs")(function* (
windowStartMs: number,
settings: ServerSettingsValue,
) {
// The home resolvers ask for `Path` themselves; satisfy them from the
// instance we already hold so the scan stays context-free.
const dirs = yield* resolveTranscriptDirs(settings).pipe(
Effect.provideService(Path.Path, path),
);
const scanned: ScannedDir[] = [];
for (const { provider, dir, fileName } of dirs) {
const volumeId = yield* Effect.promise(() => readDirectoryVolumeId(dir));
const exists = yield* fileSystem
.exists(dir)
.pipe(Effect.catchCause(() => Effect.succeed(false)));
if (!exists) {
scanned.push({ provider, dir, volumeId, files: null });
continue;
}
const files = yield* Effect.promise(() =>
listTranscriptFiles(dir, windowStartMs, fileName === undefined ? undefined : { fileName }),
);
const parsedFiles: { path: string; records: readonly UsageRecord[] }[] = [];
for (const file of files) {
const records = yield* readFileRecords(file.path, file.size, file.mtimeMs, provider);
parsedFiles.push({ path: file.path, records });
}
scanned.push({ provider, dir, volumeId, files: parsedFiles });Work the accounting lab by hand
The lab keeps the two lanes visible. Its historical miniature has one duplicate
inside a Claude file, one resumed/forked duplicate across files, and a distinct
Codex record. Advance the stages to see why the final total is 218 tokens, while a
live 88,000 / 200,000 context snapshot remains deliberately outside that total.
A file-derived total is built in stages
Move through the historical lane. The live context sample stays visible as a deliberately excluded operational signal.
T-9 currently reports 88,000 / 200,000 context tokens. It changes the composer meter only; it contributes 0 tokens and $0 to the historical total below.Stage 1 of 6 · parse records
Four candidate records arrive from three files
The scanner streams provider JSONL. These are candidate usage records, not yet a total.
| File | Record | Dedupe identity | Tokens | Status |
|---|---|---|---|---|
claude/a.jsonl | C-1a | m-1:r-1 | 160 | first copy |
claude/a.jsonl | C-1b | m-1:r-1 | 160 | within-file duplicate |
claude/b.jsonl | C-1c | m-1:r-1 | 160 | cross-file copy |
codex/x.jsonl | X-1 | none · parser-local suppression | 58 | distinct record |
- Candidate tokens
- 538
- Historical total
- not calculated
- Live meter input
- excluded
Formulas used by the lab
processed = uncached input + cached input + cache creation + output
cost = uncached×inputRate + cached×cacheReadRate + creation×creationRate + output×outputRate
historical total = Σ kept, in-range records after source-fingerprint ownership
reasoning ⊆ output; do not add it a second time
Static walkthrough and no-JavaScript reference
- Parse. Candidate rows total 538 tokens: three 160-token Claude copies and one 58-token Codex record.
- Normalize. Claude's usage maps to
100 uncached + 30 cached + 10 creation + 20 output = 160. Codex's input includes cache, so it maps to40 uncached + 10 cached + 0 creation + 8 output = 58. - Within-file dedupe. Keep
C-1a; dropC-1b, which repeats the same Claude message/request key ina.jsonl. - Cross-file dedupe. Keep the first surviving Claude key; drop
C-1cfrom the copied/resumed file. Codex has no global key here: its parser-local delta/fork suppression has already accepted distinct recordX-1. The subtotal is 218 tokens. - Bucket and price. Place the two retained records in their day/provider/model buckets. The displayed formula applies model rates only when no finite provider-reported cost exists; price provenance remains part of the result.
- Merge environments. Environment A owns the Claude source fingerprint. Environment B reports the identical fingerprint and is a shadow copy, so its Claude bucket is excluded. Environment C owns a different Codex fingerprint. The merged historical total remains 218 tokens.
Still excluded: the live 88,000 / 200,000 thread context snapshot. It answers a capacity question, not this historical file-accounting question.
Time, sessions, and scope are constraints—not decoration
- A daily range is inclusive by local day in the supplied IANA zone. An hourly
range uses exact
[since, until)instants and can span at most 24 hours. Invalid IANA input falls back to UTC in the aggregation helper, so callers should supply a real zone rather than infer one after the fact. - Transcript session totals describe distinct contributing source sessions. They do not establish a one-to-one mapping to T3 threads, provider native sessions, live context snapshots, invoices, or a user’s total work.
- Scan-home resolution uses legacy Claude/Codex provider settings. It is not a registry-wide scan of every possible provider instance configuration.
- The scan cache trades repeated parsing for path/size/mtime reuse. It is local implementation state, not an immutable evidence store; cross-file dedupe still runs after cached records are read.