You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The Usage page cost report (/devtools/usage, backed by data/usage.json) shows negligible Grok and Antigravity token usage even when those CLIs run substantial --review-with code-review passes (and as first-class CoS/toolkit providers). Quota meters can still look “used” because they scrape each CLI’s /usage panel; the persistent cost ledger is a different store and is what is wrong.
Goal: every Grok / Antigravity (and same-class nested CLI) session that ran in a PortOS workspace during a PortOS run is discovered from the harness’s local session files and attributed to that provider in data/usage.json — not discarded, not swallowed into the parent’s Claude/Codex bucket, and not left as chars/4 of the parent’s initial task blurb.
Context
Two independent systems share /devtools/usage. Mixing them up is how this looks fine on quota cards and empty on tokens:
Cost / token table — GET /api/usage → server/services/usage.js (data/usage.json). Written by recordCompletedRunUsage (server/services/usageReconciler.js:515). This is the undercount. The page footer already admits estimated rows “understate real usage substantially” (client/src/pages/UsagePage.jsx:832-838).
How tokens get into usage.json today
recordCompletedRunUsage(metadata, output):
Estimate: tokensIn = ceil(promptLength / 4), tokensOut = ceil(stdout.length / 4), cache = 0, source: 'estimate' (usageReconciler.js:518-521, contextBudget.js). CoS promptLength is only task.description.length (agentRunTracking.js:51), not the built agent prompt.
Else keep the estimate. transcriptFamily is explicitly null for agy, grok, ollama, lmstudio, kimi (usageReconciler.js:169-184, pinned in usageReconciler.test.js).
Call sites: CoS completeAgentRun records on success and failure (agentRunTracking.js:162). Toolkit onRunCompleted records on success only (bootstrap.js:171-176).
Why nested Grok/Antigravity reviews are invisible
A CoS task that reviews with grok/agy does not spawn a grok/agy PortOS run.
task:ready → spawnAgentForTask → createAgentRun(PARENT provider)
→ parent CLI is told to run /do:pr --review-with grok,antigravity,…
→ lib/slashdo/lib/local-agent-review-loop.md Step 2
grok --permission-mode bypassPermissions -p "$LOCAL_PROMPT"
agy --dangerously-skip-permissions --model "…" --print-timeout 30m -p "$LOCAL_PROMPT"
→ stdout → temp LOG_FILE the parent parses
→ no createAgentRun, no recordCompletedRunUsage for grok/agy
→ parent completes → recordCompletedRunUsage(PARENT metadata, parent stdout)
Prompt injection: server/services/agentPromptBuilder.js (~308–347, ~841). Follow-up review agents (spawnReviewLoopFollowUp in server/services/agentWorktreeCleanup.js:816) inherit the source task’s provider; they bash-launch reviewer CLIs rather than becoming grok/agy CoS agents.
Same-family nested Claude/Codex can piggyback (same transcript dir + window). Nested grok/agy never can. Cross-family nested Claude/Codex is also dropped: a grok parent that shells out to codex review writes Codex rollouts, but transcriptFamily(grok) is null so those files are never read.
What the harnesses already write (0 tokens to read)
updates.jsonl — session/update / _x.ai/session/update. Streaming chunks carry _meta.totalTokens (context-window occupancy — jumps when a tool result is added; not billed). On sessionUpdate: "turn_completed" the payload has real billed counts:
Those usage totals are cumulative for the session (same hazard as Codex total_token_usage). Summing every turn_completed inflates badly. Take the last snapshot in the run window minus a per-session high-water mark.
chat_history.jsonl — fallback when no turn_completed (killed/interrupted run): user / assistant / reasoning / tool_result with model_id. chars/4 estimate.
PortOS pins grok headless to --output-format plain (server/lib/grok.js:91-92); stdout is not the source of truth.
brain/<id>/.system_generated/logs/transcript.jsonl — steps (PLANNER_RESPONSE, tool calls, created_at); no token fields. chars/4 of textual step content.
agy --output-format is stripped as unsupported (server/lib/aiToolkit/internal/antigravity.js)
Cursor / Kimi sit in the same transcriptFamily === null hole. Include them in the sibling-family scan only if a durable cwd-keyed session store is found during implementation; do not block grok/agy on that.
Backfill as written cannot repair this
usageBackfillWorker.js:41-44 skips !transcriptFamily(metadata) and already-usageReconciled runs. applyHistoricalUsageCorrections (usage.js:331-379) further requires !reconciledRuns[runId] and a pre-existing dailyActivity[day].byProvider[parentProviderId] bucket, then adds every measured record to that parent bucket. Sibling grok rows stuffed into the parent’s measured array would land on Claude. isMeasured (usageBackfillWorker.js:12-13) would also drop Antigravity’s transcript-backed estimates.
At every PortOS run completion, scan every transcript family in that workspace + time window, not only the parent’s family. Attribute each session to its provider. A Claude parent that bash-launched grok records a grok row; a grok parent that bash-launched Codex records a Codex row. The per-message / per-session claim ledger stays the exclusivity mechanism.
Map a sibling family to the install’s matching enabled provider via familyForProvider (server/lib/providerFamilies.js: claude / codex / agy / grok). Prefer type === 'cli' over tui. If several cli records exist, prefer the one whose default/selected model matches modelUsage / model_id; else the first enabled cli. Skip the family if none match — do not invent an unknown grok bucket.
Grok parser: correlate by URL-encoded cwd directory + summary.json timestamps vs run [startTime, endTime] ± existing WINDOW_SLACK_MS. Prefer last turn_completed.usage in the window minus per-session high-water (Codex cumulative pattern). Map inputTokens → tokensIn, outputTokens + reasoningTokens → tokensOut, cachedReadTokens → cacheReadTokens, cacheCreationTokens → cacheWriteTokens, modelUsage → byModel. source: 'measured'. Do not sum _meta.totalTokens. Fallback: chars/4 of chat_history.jsonl (user+tool_result → in, assistant+reasoning → out), source: 'estimate'. Claim key = session id (plus prompt id when windowing turns).
Antigravity parser: correlate history.jsonl.workspace with cwdMatches (usageReconciler.js:118-122) and timestamp to the run window; chars/4 the matching brain transcript’s textual steps. Model = provider defaultModel (use a settings-change line if one names a model; do not regex-hunt the prompt). source: 'estimate'. Claim key = conversationId.
First-class grok/agy PortOS runs go through the same parsers via transcriptFamily returning 'grok' / 'agy'.
Backfill: sibling-family scan must run even on already-reconciled parent runs. applyHistoricalUsageCorrections must route each record to record.providerId (creating the day bucket if needed), subtract the parent estimate only from the parent provider, and accept sibling-add corrections that have no parent estimate to remove. isMeasured must not drop transcript-backed estimates. Historical repair stays the user-triggered POST /api/usage/backfill — no boot-time / silent backfill.
Keep the prompt/stdout estimate fallback when no session matches. A usage-accounting failure must never fail the run.
Do not change grok --output-format plain or agy argv as the primary source. Session files exist regardless of stdout format.
Proposed approach
server/lib/providerTranscriptUsage.js — add parseGrokSession and parseAgyTranscript (pure, truncated-line tolerant, byModel + countedKeys + exclude, same return shape as Claude/Codex plus source). Tests next to the existing parser tests. Pin both grok traps: occupancy _meta.totalTokens is not billed, and turn_completed.usage is cumulative (a two-turn fixture must not double).
server/services/usageReconciler.js
Extend transcriptFamily to 'grok' / 'agy' (and 'cursor' only if a store was found).
Split “reconcile parent family” from “collect sibling-family sessions in this window”.
Resolve sibling family → enabled provider as in decision 3 (listProviders() / familyForProvider; not getAllProviders()’s envelope).
reconcileRunUsage returns the parent record(s) plus sibling records. recordRunUsage already accepts an array and keys each entry by providerId.
recordCompletedRunUsage — unchanged caller contract; internally does the sibling scan. CoS and toolkit completion both flow through here.
usageBackfillWorker.js + applyHistoricalUsageCorrections — sibling pass on already-reconciled runs; per-record provider routing; do not require a pre-existing parent day bucket for a sibling-only add; do not drop source: 'estimate' sibling rows.
Claim ledger — grok session id / agy conversationId (and Codex/Claude message keys as today) so two overlapping PortOS runs in one worktree cannot both bill one nested review. Grok high-water sits next to the existing Codex high-water map.
Tests (boundary)
First-class grok run with turn_completed → grok row, source: 'measured', cache tiers populated, model from modelUsage.
Grok run with only chat_history.jsonl → source: 'estimate'.
Claude parent + grok session in the same cwd/window → tworecordRunUsage entries (claude measured, grok measured/estimate). Nested grok tokens must not appear under Claude.
Backfill sibling scan on an already-reconciled Claude run routes the grok row to the grok provider.
No matching session → existing prompt/stdout estimate, run still succeeds.
Usage page footer (UsagePage.jsx:832-838) — Grok rows can be Measured when turn_completed exists; Antigravity rows stay Estimated (transcript chars/4). Do not imply billed counts for agy.
Acceptance criteria
A CoS or toolkit run whose provider is Claude/Codex and whose workspace contains a Grok session overlapping the run window records a Grok cost-report row; those tokens are not added to the Claude/Codex row.
The same for an Antigravity history.jsonl + brain transcript overlapping that window (Estimated).
A first-class Grok PortOS run with turn_completed.usage records source: 'measured' (including cache tiers and modelUsage); without it, chars/4 of chat_history.jsonl is Estimated.
A first-class Antigravity PortOS run records tokens from its brain transcript, not from task.description + TUI stdout alone.
_meta.totalTokens occupancy is never billed; multiple turn_completed snapshots in one session are not summed.
Two overlapping runs in one cwd do not double-count one nested session.
Sibling sessions are skipped (not unknown) when no enabled provider matches that family.
POST /api/usage/backfill attributes historical nested grok/agy sessions to overlapping parent runs without rewriting already-measured Claude/Codex totals and without stuffing sibling tokens into the parent provider bucket.
Quota cards (GET /api/usage/providers) are unchanged.
Parser/reconcile/backfill tests above exist; a usage-accounting failure still cannot fail the agent run.
Usage page footer names grok measured-vs-estimate and agy session estimates.
Cursor/Kimi parsers unless a durable cwd-keyed session store is found while doing grok/agy — then include them in the same sibling scan; otherwise file a follow-up with the store path.
Boot-time or scheduled silent backfill.
New usage.json fields for reasoningTokens / costUsdTicks — fold reasoning into tokensOut; PortOS keeps its own rate table.
Problem / Goal
The Usage page cost report (
/devtools/usage, backed bydata/usage.json) shows negligible Grok and Antigravity token usage even when those CLIs run substantial--review-withcode-review passes (and as first-class CoS/toolkit providers). Quota meters can still look “used” because they scrape each CLI’s/usagepanel; the persistent cost ledger is a different store and is what is wrong.Goal: every Grok / Antigravity (and same-class nested CLI) session that ran in a PortOS workspace during a PortOS run is discovered from the harness’s local session files and attributed to that provider in
data/usage.json— not discarded, not swallowed into the parent’s Claude/Codex bucket, and not left as chars/4 of the parent’s initial task blurb.Context
Two independent systems share
/devtools/usage. Mixing them up is how this looks fine on quota cards and empty on tokens:GET /api/usage/providers→server/services/providerUsage.js. Headless TUI scrape ofagy /usageandgrok /usage show(server/lib/tuiUsageScrape.js). Percent remaining/used. Already works (Add Antigravity + Grok subscription-usage adapters via headless TUI /usage scrape #2598). Out of scope.GET /api/usage→server/services/usage.js(data/usage.json). Written byrecordCompletedRunUsage(server/services/usageReconciler.js:515). This is the undercount. The page footer already admits estimated rows “understate real usage substantially” (client/src/pages/UsagePage.jsx:832-838).How tokens get into
usage.jsontodayrecordCompletedRunUsage(metadata, output):tokensIn = ceil(promptLength / 4),tokensOut = ceil(stdout.length / 4), cache = 0,source: 'estimate'(usageReconciler.js:518-521,contextBudget.js). CoSpromptLengthis onlytask.description.length(agentRunTracking.js:51), not the built agent prompt.transcriptFamily(run)is'claude'or'codex', replace that with the CLI’s own per-message counts from disk (providerTranscriptUsage.js). Claude:~/.claude/projects/<cwd-slug>/*.jsonl. Codex:~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl. Includes cache tiers. This is Usage cost report understates real AI cost by orders of magnitude: input/cache tokens are never counted and most day buckets are dropped #3124.transcriptFamilyis explicitly null foragy,grok, ollama, lmstudio, kimi (usageReconciler.js:169-184, pinned inusageReconciler.test.js).Call sites: CoS
completeAgentRunrecords on success and failure (agentRunTracking.js:162). ToolkitonRunCompletedrecords on success only (bootstrap.js:171-176).Why nested Grok/Antigravity reviews are invisible
A CoS task that reviews with grok/agy does not spawn a grok/agy PortOS run.
Prompt injection:
server/services/agentPromptBuilder.js(~308–347, ~841). Follow-up review agents (spawnReviewLoopFollowUpinserver/services/agentWorktreeCleanup.js:816) inherit the source task’s provider; they bash-launch reviewer CLIs rather than becoming grok/agy CoS agents.Same-family nested Claude/Codex can piggyback (same transcript dir + window). Nested grok/agy never can. Cross-family nested Claude/Codex is also dropped: a grok parent that shells out to
codex reviewwrites Codex rollouts, buttranscriptFamily(grok)is null so those files are never read.What the harnesses already write (0 tokens to read)
Grok —
~/.grok/sessions/<urlencoded-cwd>/<session-id>/:summary.json—created_at,last_active_at,current_model_id,git_root_dir,num_messagesupdates.jsonl—session/update/_x.ai/session/update. Streaming chunks carry_meta.totalTokens(context-window occupancy — jumps when a tool result is added; not billed). OnsessionUpdate: "turn_completed"the payload has real billed counts:{ "sessionUpdate": "turn_completed", "usage": { "inputTokens": 15258, "outputTokens": 1828, "totalTokens": 17086, "cachedReadTokens": 11264, "cacheCreationTokens": 0, "reasoningTokens": 848, "modelUsage": { "grok-4.5-build": { "inputTokens": 15258, "outputTokens": 1828 } } } }Those
usagetotals are cumulative for the session (same hazard as Codextotal_token_usage). Summing everyturn_completedinflates badly. Take the last snapshot in the run window minus a per-session high-water mark.chat_history.jsonl— fallback when noturn_completed(killed/interrupted run):user/assistant/reasoning/tool_resultwithmodel_id. chars/4 estimate.PortOS pins grok headless to
--output-format plain(server/lib/grok.js:91-92); stdout is not the source of truth.Antigravity —
~/.gemini/antigravity-cli/:history.jsonl—{ timestamp, workspace, conversationId }(cwd + time correlation)brain/<id>/.system_generated/logs/transcript.jsonl— steps (PLANNER_RESPONSE, tool calls,created_at); no token fields. chars/4 of textual step content.agy --output-formatis stripped as unsupported (server/lib/aiToolkit/internal/antigravity.js)Cursor / Kimi sit in the same
transcriptFamily === nullhole. Include them in the sibling-family scan only if a durable cwd-keyed session store is found during implementation; do not block grok/agy on that.Backfill as written cannot repair this
usageBackfillWorker.js:41-44skips!transcriptFamily(metadata)and already-usageReconciledruns.applyHistoricalUsageCorrections(usage.js:331-379) further requires!reconciledRuns[runId]and a pre-existingdailyActivity[day].byProvider[parentProviderId]bucket, then adds everymeasuredrecord to that parent bucket. Sibling grok rows stuffed into the parent’smeasuredarray would land on Claude.isMeasured(usageBackfillWorker.js:12-13) would also drop Antigravity’s transcript-backed estimates.Related issues (not duplicates)
Decisions (locked)
Do not re-litigate these. File a follow-up if a live install proves one wrong.
familyForProvider(server/lib/providerFamilies.js:claude/codex/agy/grok). Prefertype === 'cli'overtui. If several cli records exist, prefer the one whose default/selected model matchesmodelUsage/model_id; else the first enabled cli. Skip the family if none match — do not invent anunknowngrok bucket.summary.jsontimestamps vs run[startTime, endTime]± existingWINDOW_SLACK_MS. Prefer lastturn_completed.usagein the window minus per-session high-water (Codex cumulative pattern). MapinputTokens→tokensIn,outputTokens + reasoningTokens→tokensOut,cachedReadTokens→cacheReadTokens,cacheCreationTokens→cacheWriteTokens,modelUsage→byModel.source: 'measured'. Do not sum_meta.totalTokens. Fallback: chars/4 ofchat_history.jsonl(user+tool_result→ in,assistant+reasoning→ out),source: 'estimate'. Claim key = session id (plus prompt id when windowing turns).history.jsonl.workspacewithcwdMatches(usageReconciler.js:118-122) andtimestampto the run window; chars/4 the matching brain transcript’s textual steps. Model = providerdefaultModel(use a settings-change line if one names a model; do not regex-hunt the prompt).source: 'estimate'. Claim key =conversationId.transcriptFamilyreturning'grok'/'agy'.applyHistoricalUsageCorrectionsmust route each record torecord.providerId(creating the day bucket if needed), subtract the parent estimate only from the parent provider, and accept sibling-add corrections that have no parent estimate to remove.isMeasuredmust not drop transcript-backed estimates. Historical repair stays the user-triggeredPOST /api/usage/backfill— no boot-time / silent backfill.--output-format plainor agy argv as the primary source. Session files exist regardless of stdout format.Proposed approach
server/lib/providerTranscriptUsage.js— addparseGrokSessionandparseAgyTranscript(pure, truncated-line tolerant,byModel+countedKeys+exclude, same return shape as Claude/Codex plussource). Tests next to the existing parser tests. Pin both grok traps: occupancy_meta.totalTokensis not billed, andturn_completed.usageis cumulative (a two-turn fixture must not double).server/services/usageReconciler.jstranscriptFamilyto'grok'/'agy'(and'cursor'only if a store was found).listProviders()/familyForProvider; notgetAllProviders()’s envelope).reconcileRunUsagereturns the parent record(s) plus sibling records.recordRunUsagealready accepts an array and keys each entry byproviderId.recordCompletedRunUsage— unchanged caller contract; internally does the sibling scan. CoS and toolkit completion both flow through here.usageBackfillWorker.js+applyHistoricalUsageCorrections— sibling pass on already-reconciled runs; per-record provider routing; do not require a pre-existing parent day bucket for a sibling-only add; do not dropsource: 'estimate'sibling rows.turn_completed→ grok row,source: 'measured', cache tiers populated, model frommodelUsage.chat_history.jsonl→source: 'estimate'.recordRunUsageentries (claude measured, grok measured/estimate). Nested grok tokens must not appear under Claude.turn_completednot double-counted;_meta.totalTokensignored.transcriptFamily({ providerId: 'agy' }) === 'agy'.UsagePage.jsx:832-838) — Grok rows can beMeasuredwhenturn_completedexists; Antigravity rows stayEstimated(transcript chars/4). Do not imply billed counts for agy.Acceptance criteria
history.jsonl+ brain transcript overlapping that window (Estimated).turn_completed.usagerecordssource: 'measured'(including cache tiers andmodelUsage); without it, chars/4 ofchat_history.jsonlisEstimated.task.description+ TUI stdout alone._meta.totalTokensoccupancy is never billed; multipleturn_completedsnapshots in one session are not summed.unknown) when no enabled provider matches that family.POST /api/usage/backfillattributes historical nested grok/agy sessions to overlapping parent runs without rewriting already-measured Claude/Codex totals and without stuffing sibling tokens into the parent provider bucket.GET /api/usage/providers) are unchanged.Out of scope
providerUsage.js, Add Antigravity + Grok subscription-usage adapters via headless TUI /usage scrape #2598).--output-formator teaching agy a JSON usage stream as the primary source.executeApiRunSSEusagefields, Codex app-serverthread/tokenUsage/updated.usage.jsonfields forreasoningTokens/costUsdTicks— fold reasoning intotokensOut; PortOS keeps its own rate table.Open questions
None — decisions above are locked.