A settings mutation replaces the resident serve child, which may be mid-write:
the same lock hazard a timed-out read has, so it gets the same treatment.
restartAfterMutation and destroy now go through killGracefully instead of
SIGKILL. reapAll's one-shot loop stays a hard kill and now says why - quit has a
1.5s flush budget, shorter than the grace, so waiting one out would wedge the
quit, and a one-shot read holds no lock worth releasing.
The desktop app capped every read at 45s of TOTAL runtime, so a slow machine's
warm `optimize` (52.5s on 0.9.20, 39.6s on main) was SIGKILLed mid-parse and the
panel painted a red "timed out" that a 60s poll then reproduced forever.
- cli.ts: the read timeout is now a no-output watchdog. The window restarts on
every byte the child writes (serve: every frame carrying that request's id),
so only a genuinely silent child times out. A 15-minute absolute ceiling still
reaps a livelocked child. Every read spawn sets CODEBURN_PROGRESS=1 so a long
parse heartbeats through it; progress lines are stripped from any surfaced
stderr so they can never become the error message.
- cli.ts: a timed-out child gets SIGTERM first and SIGKILL only after a 5s
grace, on both the spawn and the serve branch. That lets the CLI's signal
cleanup publish its partial parse and release the cross-process refresh lock
instead of leaving both stale.
- main.ts: the 10-minute cold floor now covers EVERY read while the first
hydration is still running, not just the overview. Sections start polling the
moment `ready` flips (which an overview error also does), and a 45s section
spawn queued behind the cold parse was killed on arrival.
- main.ts/renderer: a timeout raised while the hydration is still running is
flagged `cold`, and the renderer keeps its indexing splash (and keeps the
sections gated) instead of latching ready and painting CliErrorPanel. Only
timeouts are flagged; a permission or nonzero failure still surfaces.
- cli.ts/main.ts: the resident serve child's pid is recorded under userData and
a serve orphaned by a previous crash is reaped at launch, after `ps` confirms
the pid still belongs to a codeburn serve.
- src/main.ts: `codeburn serve` exits explicitly once its stdin closes, so a
handle outliving the transport cannot keep the child alive as an orphan.
The merge with main pulled in #1056's codex-auto-review -> gpt-5.5
alias, which findUnpricedModels' pre-existing hasBillableRate check
now resolves for the raw id, so a synthetic $0 row for it is no
longer reported as unpriced (it correctly has a billable rate).
Removed it from the expected findUnpricedModels() output in the
"skips subscription / flat-rate product SKUs" test; the display-name
variant ('Codex Auto Review') is unaffected since getModelCosts does
not resolve display names, so it stays in the expected list. The
codex-auto-review / #1056 pricing interaction itself is already
covered by the "Codex activity ids (#1047)" describe block.
Resolves conflicts against main after #1078/#1084/#1088/#1090/#1092/
#1053/#1056 landed since this branch's last upstream merge.
- CHANGELOG.md: kept both entries (this PR's #968/#1050 note plus
main's #1079/#1088, #1082, #1075 notes that had moved into the same
"### Fixed" slot).
- Everything else (src/models.ts, main.ts, usage-aggregator.ts,
daily-cache.ts, tests/*) merged cleanly with no conflict markers;
git's recursive merge combined #1050's flat-rate classifier changes
with #1056's codex-auto-review -> gpt-5.5 alias without overlap.
Verified the codex-auto-review / #1056 interaction post-merge:
isBuiltInFlatRateModel no longer matches codex-auto-review (dropped
per #1050), while MODEL_ALIASES still aliases it to gpt-5.5 (#1056),
so it prices at GPT-5.5 rates rather than $0. Covered by the existing
tests/models.test.ts "Codex activity ids (#1047)" describe block and
the "does not treat a priced sibling as expected-free" case.
getFlatRateModelsConfigHash's output is folded into
getDailyCacheConfigHash's template literal unconditionally
(flatRateModels=<hash>), so the flat-rate section always participates
in the daily-cache invalidation hash even when empty -- no
DAILY_CACHE_VERSION bump needed for this change.
Resolves conflicts from main's #1078 (billableOutputTokens for the
output bucket) and #1084 (models-report test exemplar swap) against
this branch's canonical-id row merging.
- src/audit-report.ts: import-line collision only. Union both sides'
imports (billableOutputTokens + fallbackRawModelDisplayName/
getShortModelName); both are used elsewhere in the file and neither
side's logic needed further changes.
- src/models-report.ts: import-line collision resolved the same way.
The row-construction conflict was structural, not a data conflict:
main's side pushed one row per raw bucket (a simpler variant this
branch's canonical-id folding already made obsolete downstream --
rowsByKey/foldedCategoryCost/foldKey are used unconditionally past
this point). Kept this branch's rowsByKey merge-by-canonical-id
structure, which already computes credits from bucket.outputTokens
(already billable-output-summed at the accumulation stage, main's
change, untouched by this conflict) using the identical formula
main used; folded main's explanatory comment about billable output
into the kept credits block.
Main already spent Codex results v14 (#1092). Daily stays 25
(#1090 still the last daily bump). Keep all four parse-version
tokens (codex-pricing-v1 + codex-tps-v1 + codex-mcp-skills-v1 +
activity-price-v1) so a take-ours merge cannot drop the pricing,
throughput, or MCP/skills invalidation. Do not Extra High again.
The `mcp-cli call <server> <tool>` matcher from #656 only ran on
`function_call` arguments. Codex records the same exec two other ways:
the custom-tool transport carries the shell tool as a `custom_tool_call`
whose payload is an `input` program (no `arguments` at all), and the item
model repeats a finished command as `event_msg`/`item_completed` with a
`CommandExecution` item holding an argv `command`. Both reach the Bash
counter and neither reached the matcher, so a CLI-wrapped MCP call stayed
missing from the MCP breakdown (#478).
All three shapes now feed one classification pipeline. The same pipeline
learns skills: Codex has no skill tool, so a skill load is a shell read of
the skill's SKILL.md, which landed entirely under Bash. A read counts only
when the command segment starts with a file-reading binary and the path
ends in `<name>/SKILL.md`; the skill is `<name>`, matching pi's key (#588)
and the Claude parser's vocabulary. A grep near a SKILL.md stays Bash.
Attribution only: the item-model path adds no tool of its own, and a
command carried by both shapes is attributed once (per-name claim ledger).
Cost, tokens, calls and daily rollups cannot move.
CODEX_CACHE_VERSION 13 -> 14 and the codex parse version both move: both
layers store tools/toolSequence/skills verbatim, and on a warm cache the
fix is otherwise invisible (verified by reverting the bumps: skills stay
empty). DAILY_CACHE_VERSION is deliberately unchanged - the daily cache
holds no tool/mcp/skill breakdown, and turn category cannot change because
every added attribution rides on a call that already has Bash.
Main already spent Codex results v13 (#1079) and daily v24 (#1090).
Keep all three parse-version tokens (codex-pricing-v1 +
codex-tps-v1 + activity-price-v1) so a take-ours merge cannot
drop the pricing or throughput invalidation. Do not Extra High again.
Re-instrumenting this exact head over the full corpus traced the
original 12.5%/46% figures to a replication gap: the earlier
prevCumulativeTotal guard (codex.ts) already discards any token_count
event whose running total exactly repeats the previous kept one, so a
drop at the seenKeys dedup site is always a byte-identical replay of
already-counted tokens -- never a real loss. The condition BUG-2's fix
guarded against does not occur in real Codex output.
Removes taskDedupedTokens, its increment at the dedup site, the
active-time scaling at task_complete, and the now-unexercisable unit
test (its fixture forces a dedup collision that is not also a
cumulative-total repeat, a state the real writer never produces). The
dedup site keeps a short comment recording why no rescaling is needed,
so the next investigator doesn't retrace this.
Keeps: the reasoning double-count fix, the harness-startup exclusion
(BUG-1, confirmed to the decimal), the shared mergeToolIntervals
helper and permissive duration parsing (BUG-8), and the legend rename.
The real-corpus table is unchanged -- BUG-2 measured zero effect on it
before this revert too.
Extends #1079 (reasoning double-count) with three more findings from an
exactness pass over the same throughput path:
- BUG-1: task_started fires before Codex assembles the request, so the
gap to the first request-context event (turn_context, world_state,
event_msg/user_message, or response_item/message) was counted as
active model time. The active window now starts at that event instead.
- BUG-2: a token_count event dropped by fork-replay dedup lost its
tokens from the numerator while the task's real duration still spanned
it in the denominator, understating Tok/s for a partially (not fully)
deduped task. The active window is now scaled down by the dropped
tokens' proportional share.
- BUG-8: the tool-interval clip/merge/cap logic was duplicated between
providers/codex.ts and codex-throughput.ts and had already drifted
(task_complete only read a plain-number duration, unlike
mcp_tool_call_end). Now one shared mergeToolIntervals, and
task_complete's duration parses the same permissive forms.
Cost and every token count remain byte-identical. activeGeneratedTokens/
activeDurationMs/toolWaitMs are stored verbatim in both Codex caches, so
none of this self-heals -- but the CODEX_CACHE_VERSION 13 bump already
shipped for #1079 covers the same fields, so no further bump is needed.
Dashboard's per-model column keeps the "Tok/s" header (zero width slack
at the standard layout, verified against a real test); the legend now
spells out "Effective Tok/s" with a decode-speed disclaimer.
Neither id is in LiteLLM yet, so litellm-snapshot.json had no row for
them (285 sessions / 5,446 calls priced at $0 on one real corpus,
flagged during #1075 verification). Add explicit snapshot rows sourced
from the pattern every prior Codex-suffixed id LiteLLM does carry
follows: it bills identically to its bare-model sibling of the same
generation (gpt-5-codex == gpt-5, gpt-5.1-codex == gpt-5.1-codex-max ==
gpt-5.1, gpt-5.2-codex == gpt-5.2, gpt-5.3-codex == gpt-5.3, verified
against the live model_prices_and_context_window.json) - both new ids
get the exact gpt-5.6 tuple rather than an invented rate.
getModelCosts already resolved both ids to the correct rate through
the `gpt-5.6` prefix fallback before this, so a fresh parse or a warm
session-cache read (codeburn models included) was already pricing
these calls correctly. The daily cache is the one place that does not
self-heal: it has no per-provider invalidation, so a day finalized
before a `gpt-5.6` snapshot row existed at all keeps that $0 forever.
Raising MIN_SUPPORTED_VERSION (v23 -> v24) forces the one-time
re-derivation, a lossless no-op for days already correct - the
mechanism itself is generic and already covered by
daily-cache-version-rederivation.test.ts. #1056 also claims 24 on its
own branch; whichever lands second takes the next number.
Closes#1077
The same reasoning-inclusion bug #1075/#1078 fixed for cost also affected
the throughput display: activeGeneratedTokens/taskGeneratedTokens in the
Codex parser and generatedTokens in the live codex-tps reader summed
outputTokens + reasoningTokens, but reasoning is already inside output.
All three sites now route through the billableOutputTokens('codex', ...)
helper #1078 introduced, so the throughput numerator can never drift from
the billed one.
Cost and every token count are unchanged (verified byte-identical on a
real 51,753-call corpus); Tok/s drops 20-42% depending on how
reasoning-heavy the model is.
activeGeneratedTokens/activeDurationMs/toolWaitMs are stored verbatim in
both the Codex result cache and the session cache rather than re-derived
on read, so neither self-heals: CODEX_CACHE_VERSION moves 11 -> 13 (12 is
claimed by feat/core-extraction's own port of this feature) and
PROVIDER_PARSE_VERSIONS.codex gains a codex-tps-v1 suffix, forcing Codex
sessions to re-parse once.
Closes#1079
Main already spent Codex results v11 and daily v23 on #1075, and
result caches are now version-suffixed. Keep both parse-version
tokens (codex-pricing-v1 + activity-price-v1) so a take-ours merge
cannot drop the pricing invalidation. Do not Extra High again.
Mixed-version binaries were clobbering the unsuffixed Codex, Cursor,
and Antigravity result files and each re-parsed the whole corpus.
Write *-results.v<n>.json like the daily cache. Leave the unsuffixed
file for older binaries; adopt a matching-version copy once.
The COST_CHANGED_BY_DESIGN carve-out in compare.mjs left codex cost entirely
unasserted after #1075/#1078. It now requires the upgraded cost to be strictly
lower than baseline and within 25% of it, and the row verdict says "repriced"
instead of the misleading "identical (cost N% drift)".
grok.ts's comment on the reasoning/output split still claimed provider-side
splitting was the repo's only mechanism; it now also names
billableOutputTokens/REASONING_INCLUDED_IN_OUTPUT (models.ts), which is the
other half since #1078. usage-aggregator.ts's "folds reasoning into output"
comment was true pre-#1078 but is backwards for codex now (reasoning is
already inside output, not added to it).
Test exemplars for the "reasoning is additive" case used hermes, whose
upstream is OpenAI-shaped and may not stay a safe example; swapped to gemini,
which documents "thoughts" as genuinely separate output.
CHANGELOG's #1075 entry gets one line noting days whose codex transcripts
have aged out keep their pre-fix totals via the daily-cache never-lose guard,
matching the disclosure already given for #1040.
The pricing cache written to disk had no schema version, so a cache written
by a pre-#1078 binary lacked cacheWriteCostIsExplicit on every entry. Reading
it back resolved the missing key to undefined (falsy), silently reintroducing
the surcharge-fabrication bug #1078 killed for up to CACHE_TTL_MS after an
upgrade. loadCachedPricing now rejects any cache whose version doesn't match
the current schema instead of reading it verbatim.
codexCredits() still accepted an optional reasoningTokens param that added it
to output - the exact double-count #1078 removed from every real caller. The
only caller never passed it; deleted it so it can't be reintroduced by
accident.
parser.ts's activeGeneratedTokens fallback went through billableOutputTokens
in #1078, but codex is the only caller of activeDurationMs/activeGeneratedTokens
and always sets both together, so the fallback branch is unreachable for it.
Reverted to reduce diff noise.
Reasoning tokens are a subset of output_tokens for OpenAI models, not an
extra bucket: on a 1,396-rollout corpus all 134,316 token_count events
carrying a total satisfy input + output == total. CodeBurn added
reasoning_output_tokens on top when pricing a codex call, in the
cache-rehydration re-price, and in the models/audit display sums. That
overstated codex cost by $166.03 (3.5%) and displayed output tokens by
34.6% on that corpus. Both cost sites and the display sums now go through
one shared billableOutputTokens() so a cold parse and a warm read cannot
drift apart.
cache_write_input_tokens (codex PR #33454) was never read and
cacheCreationInputTokens was hardcoded to 0. It is now carved out of the
uncached-input bucket and clamped to it, but routed to the cache-write
bucket ONLY when the pricing source publishes an explicit cache-write rate.
buildCosts fabricates 1.25x input when a source omits one, which is correct
for Anthropic and would have invented a surcharge OpenAI never charged on
gpt-5.5 / 5.4 / 5.3-codex / gpt-5. ModelCosts now carries
cacheWriteCostIsExplicit so that distinction survives getModelCosts.
A cost change invalidates persisted output: codex-results.json v10 -> v11
(stores costUSD verbatim), the codex parse version moves (the token-bucket
change does not self-heal on read), and the daily cache goes 20 -> 23 (21 is
claimed by the #946 landing branch and 22 by PR #1056). The upgrade-path
corpus asserts codex tokens and calls exactly and reports the repricing.
Closes#1075
Extra High MERGE AFTER FIX on 7e51413. JSON-report headlines
come from durable.data; daily[] is the durable.days consumer.
Oracle helper and assertions unchanged.
#1070 is hygiene, not a leak. User docs no longer name
deriveTraceId or list session ids under What is NOT sent.
Code comment matches the wire key. Class test pins every
attribution span: no ai.session_id, join is shared traceId.
Attribution spans already share deriveTraceId(sessionId) with
usage spans. Emitting the raw session id was redundant and
undid the pre-attribution wire rule that session id is hash
input only. Receivers upsert session spans by that keyed
traceId. Usage spans never had the field. Docs match.
Extra High MERGE AFTER FIX on 1fa284f. The parity helper is an
independent per-call oracle for durable.days, not a mirror of a
live dailyMap fallback that no longer exists. Helper and test
unchanged.
buildJsonReport always received a DurablePeriod from both JSON
call sites, so the live dailyMap fallback could never run (#1067).
Make durable required and take headlines + daily rows only from
durable.days. Proxied/net split stays live-session. No fallback.
Extra High MERGE AFTER FIX on a3beafb. The process tests admitted
completed-by-other as a non-owner outcome, but the workflow still
had continue-on-error and a quarantined step name. The suite now
gates. Still serial (parallelism-sensitive). No tryTakeover rewrite.
The process suite required the loser of a stale-lock contest to
be timed-out. On a slow runner the winner's unlink-guard /
create-successor gap is missing+missing, which the lock honestly
reports as completed-by-other (#904).
Exactly one owner still publishes. The loser may be timed-out,
completed-by-other, or unavailable — never parsed. Do not delay
the clean-release path by treating missing+missing as wait-out.
The visible prefix that decides title-vs-id order must match the
title cap. A UTF-16 slice can split an emoji and treat two distinct
titles as the same truncated series.
Hourly session series still opened with a truncated UUID, so a
monorepo's legend was six identical hex fragments. Sessions
report already prefers title; the chart did not.
Lead with the cleaned title when that prefix is unique in the
visible ~24-glyph budget. Fall back to short-id-first when
titles collide or share a long prefix. Untitled sessions stay
project fallback. No full session ids on the happy path.
Extra High MERGE AFTER FIX: source-scraping the setup file treated a
comment containing 'HERMES_HOME' as isolation. The lists now live in a
side-effect-free module that applyIsolation and the declaration test
both import. Comment-only sabotage fails with hermes:HERMES_HOME.
HERMES_HOME and eight sibling PROVIDER_ENV_VARS data-dir overrides were
fingerprinted for cache invalidation but never CLEARED by the vitest
setup file, so a Hermes-shell laptop parsed real sessions in fixtures.
A static guard fails closed when the map grows another undeclared home.
Maintainer review on #1054: two-journal collision is unreachable
through discovery. Keying by timestamp+journal collapsed a 3-leg
stampless journal onto one row, and the :n strip re-sent ledger
keys. Keep occurrence keys. Take lastEventTimestamp before
sessionStartTime for the call timestamp only. No migration.
Maintainer review on #1053: display-name keys collide for distinct
SKUs (GPT-5 / GPT-5.3 Codex / Kimi K2 Thinking / Opus 4.6). Key on
the alias-resolved canonical id, keep the first raw id, and partial-sum
Codex credits when a merge mixes rated and unrated buckets.
Maintainer review on #1050: drop metered codex-auto-review, add
kimi-for-coding-highspeed, match Warp's auto id, drop unsourced
big-pickle, and give model-flat-rate --remove power over built-ins.