[#204] Codex CLI as a third translation tier (app-server), gated on measured overhead - #210
Conversation
Codex exposes no USD cost anywhere, but it does expose account/rateLimits/read as a percentage of allowance — which is exactly what the #205 seam consumes, so this needs no token->USD rate card and ships none. Both shapes here were measured against the real codex-cli 0.146.0 rather than read off the schema, and both would have been bugs otherwise: `secondary` is null on a real plan, so this handles 1..N windows instead of assuming two; and `resetsAt` is epoch SECONDS while the seam documents milliseconds, which would have rendered a 1970 date in the "resets in" line. Only numeric percentages and window durations cross into the seam. planType, account ids, and every other identifying field on the response are deliberately not modelled, so they cannot reach a gauge event or a log line. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
RE2 predicted that an unconverted seconds value renders a permanent "resets now" — a string that reads as correct while being wrong. The conversion was already in place; this asserts the contrast directly (raw -> "resets now", converted -> "resets in 13h") and that hours are identical either way, so scope 7 stays visibly true. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
RE2's point: #204 added windowLabel as a producer, but quotaHeadroom still built nativeDetail from usedPercent + resetsAt alone, so the field would be populated and never consumed — the same gap as before, now with machinery that makes it look like it works. The binding window is now named when there is MORE THAN ONE window, because "90% used" does not say which wall is being hit and the whole point of the minimum is that the answer isn't obvious. With a single window — what a real prolite plan reports — the label is omitted rather than padding every line with a name that disambiguates nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drives `codex app-server` over JSON-RPC/stdio: thread/start -> turn/start per turn, item/agentMessage/delta -> progressive snapshots, thread/resume for crash recovery on the SERVER-minted thread id. Never `codex exec`, which is one process per turn and returns a single completed message. The measured feature-disable set is what makes this affordable: replacing baseInstructions and disabling 29 stable-but-irrelevant coding-agent features took a translation turn from 13,727 to 5,168 input tokens (-62%) with no loss of quality. Asserted on real spawned argv, not a mock. Rollover keys on modelContextWindow rather than a cache signal Codex does not expose, because per-turn input grows ~31 tokens/turn as the thread accumulates. It is flagged during a turn and applied before the NEXT one, so an in-flight turn is never interrupted. Codex reports no USD anywhere, so Usage cost fields are 0 rather than invented; the fallback decision rides entirely on the #205 headroom seam. The startup probe is functional (initialize + thread/start must both succeed) because the wire carries no protocol-version field to compare against — only a userAgent, from which just the version digits are kept. Tested against a fake app-server speaking the real measured protocol over real stdio, so nothing here requires the codex binary to exist in CI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
RE2's catch, and a real bug rather than a cosmetic one: with a headroom source configured, estimatedHoursRemaining came from the seam but poolUsd/spentUsd/ fractionUsed still came off the USD ledger. The webview renders exactly those, so a Codex session would show "$0.00 / $20.00" with an empty bar — a full, untouched budget — for the entire session, while the actual constraint sat in nativeDetail where nothing read it. Not merely zeroed: actively reassuring and wrong. The gauge now prefers nativeDetail (which on the Claude tier IS the USD string, so both tiers take one path), and the bar's fraction comes from the headroom source. Unknown headroom shows no fill rather than an empty bar implying a full allowance. Claude behaviour is unchanged — the bit-for-bit oracle asserts fractionUsed exactly and still passes. Also adds binary-presence detection for `codex`: version probe only, never any credential or login state, and a null return hides the option entirely. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The picker gains a Codex tier alongside Claude CLI and Local, revealed only when a codex binary is present — hidden entirely rather than shown disabled, so a user without Codex never sees an option they cannot use. A stale settings.json selecting Codex on a machine that no longer has it falls back rather than leaving a hidden tier selected. Per the PO ruling, the quota cost is stated in the picker itself rather than a tooltip: that it runs on the user's own Codex/ChatGPT account and spends that plan's quota, the MEASURED bound (roughly 4-7 hours of captioning per week on the plan tested, less on a smaller one), that LiveCap never sees the login, and that it falls back to Local when quota runs low. No "unlimited" implication, and the hours figure is not rounded up. Detection is binary presence plus --version; no credential or login state is read on either side of the wire. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the codex branch alongside the Claude and local paths, mirroring the #142 two-lane split for the same reason: a summary turn must not head-of-line-block live captions. Both lanes fall back to the same shared local engine. The headroom source reads through a late-bound reference because the ordering is circular: the accountant must exist before the engine (the router's startOnFallback consults it) and the engine must exist before any rate-limit read can happen. Quota is per ACCOUNT rather than per thread, so one lane's app-server serves the reads. readRateLimits returns only the two window objects; planType, limitId, credit balances and every other field are dropped at that boundary so no account-identifying value travels further into the app. Selecting Codex without the binary falls back to local with a status, the same shape the Claude path already had. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
refreshHeadroom was primed once at engine construction, so a meeting running for hours would decide the fallback on the reading taken at session start and never notice the allowance draining. Ticks only when a headroom source exists; a failed read is already unknown-and-non-switching in the seam. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
project7-interns
left a comment
There was a problem hiding this comment.
Verdict: REQUEST CHANGES
Epic Alignment: FAIL
The adapter's rollover implementation does not satisfy #204's context-window contract: it checks each turn's input size instead of the accumulated thread context, so a long session can cross the context wall without rolling over.
Checked (evidence)
- Structural gate: PR body contains EPIC Alignment, Self-Verification, and Deviations sections; required quota/ToS caveats are present.
- Verified the app-server chain, quota source, refresh cadence, gauge display correction, and security boundary in the live diff.
- Riskiest part: context rollover based on Codex's modelContextWindow.
- Kill-list: one correctness finding below.
- CI: gh pr checks 210 -> color-guard/no-stub-gate/packages-linux pass; app-macos and release-invariants pending.
Findings
- [blocking] Rollover compares one turn's inputTokens to the context window, not the current thread's accumulated input.
- File: packages/engine/src/codex-app-server-engine.ts:476-493
- Why it fails: recordTokenUsage() computes inputTokens from tokenUsage.last and tests inputTokens >= modelContextWindow * fraction. The measured workload grows ~31 tokens per turn, so each turn stays far below 60% even as the persistent thread accumulates toward the wall; the rollover flag never fires in real long meetings. The test only passes by setting one synthetic turn to 700 against a 1000-token window.
- Do instead: track per-thread cumulative input (or use tokenUsage.total.inputTokens) and compare that to the threshold, resetting the thread counter when startThread/rollover begins; keep cumulative session usage separate for Usage reporting. Add a multi-turn regression with small per-turn input that crosses the threshold only in aggregate.
Decision
The adapter, quota conversion, refresh cadence, gauge correction, and crash-recovery paths are otherwise aligned, but this rollover bug can let a long Codex session hit its context wall without recovery. Fix it and re-request review after CI completes.
RE2 — APPROVEPR #210 (#204) @ Checked (evidence)1. My three pre-PR flags — all closed, and two better than I specified.
2. Operator requirements, each verified against the artifact.
3. 4. Refresh cadence — the gap that prompted this, closed correctly. 5. Gates. engine 336 (29 files), app 185, both typechecks exit 0, On the three-state question — the disposition is right, and the disagreement is properly recordedStale collapses to I argued for aging the reading forward instead, and I am not blocking on it. @dev's reasons for deferring are good ones and I'd have made the same call: it changes the decision semantics of an already-merged, already-reviewed seam, on a PR that is already large, and it needs an explicit asserted age bound — precisely the kind of number that silently becomes wrong when added late to a big change. What matters for this review is that the residual is written down as a weakness rather than presented as a considered optimum: a sustained refresh failure disarms the auto-switch for the rest of the session, and #205's "unknown never switches" was justified by transient failures, not sustained blindness. Two things bound it — turns must keep succeeding while only Recommend a follow-up ticket against #205 for the aging-forward option. The arithmetic is already in the seam ( Note (non-blocking)The (The shared bot token cannot file a formal GitHub approval, so this comment plus my chat message is the RE2 verdict of record.) |
RE1 read the trigger as comparing one turn's new-message tokens to the window. It isn't, and the protocol is the reason: `last.inputTokens` is the WHOLE context sent for that turn, so on a multi-turn thread it already includes every prior turn. Measured short -> LONG -> short on codex-cli 0.146.0: turn 1 (short) 5,158 turn 2 (LONG) 7,106 turn 3 (short) 7,159 <- does not fall back to ~5,158 Turn 3's short message still carries turn 2's bulk, so that single value IS the accumulated context and comparing it to the window is the correct trigger. Adds the aggregate multi-turn test RE1 asked for, driving growth across several turns until the rising context crosses the threshold, plus its converse: summing per-turn inputs must NOT trigger rollover. That converse is the bug the requested change would have introduced — 30 turns summed to 168,769 against a 258,400 window while real context was 6,088, so a sum-based trigger would roll over every ~28 turns and destroy continuity for nothing. Renames the local to contextTokens and records the measurement inline, since the old name is what invited the misreading. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
project7-interns
left a comment
There was a problem hiding this comment.
Verdict: APPROVE\n\n### Epic Alignment: PASS\nThe new commit preserves the app-server contract and now proves context-based rollover across turns without summing per-turn work.\n\n### Checked (evidence)\n- Delta reviewed: ; implementation compares whole-context and resets only on ().\n- Aggregate-growth regression: fake server's per-thread growth and 5,000→6,200 crossing test (; ).\n- Converse regression confirms summed inputs alone do not roll over ().\n- Riskiest part: protocol interpretation of ; the added measured-sequence documentation and end-to-end growth tests make the rollover semantics explicit and guard against reintroducing the prior defect.\n- Kill-list: scanned the new ranges — clean.\n- CI: app-macos pass 2m20s https://github.com/realproject7/livecap/actions/runs/30963633911/job/92172757123
color-guard pass 7s https://github.com/realproject7/livecap/actions/runs/30963633911/job/92172757079
no-stub-gate pass 5s https://github.com/realproject7/livecap/actions/runs/30963633911/job/92172757060
packages-linux pass 37s https://github.com/realproject7/livecap/actions/runs/30963633911/job/92172757126
release-invariants pass 2m1s https://github.com/realproject7/livecap/actions/runs/30963633911/job/92172757124 → color-guard, no-stub-gate, packages-linux, release-invariants, and app-macos all passing.\n\n### Findings\nNone.\n\n### Decision\nThe prior blocker is resolved at the new SHA with both positive aggregate-growth and anti-summed-input coverage. All live checks pass, so this PR is approved.
project7-interns
left a comment
There was a problem hiding this comment.
Verdict: APPROVE
Epic Alignment: PASS
The new commit preserves the app-server contract and now proves context-based rollover across turns without summing per-turn work.
Checked (evidence)
- Delta reviewed:
529b107..f016dba; implementation compares whole-contextlast.inputTokensand resets only onthread/start(packages/engine/src/codex-app-server-engine.ts:476-507). - Aggregate-growth regression: fake server's per-thread growth and 5,000→6,200 crossing test (
packages/engine/test/fake-app-server.mjs:35-45,108-130;packages/engine/test/codex-app-server-engine.e2e.test.ts:172-250). - Converse regression confirms summed inputs alone do not roll over (
packages/engine/test/codex-app-server-engine.e2e.test.ts:215-250). - Riskiest part: protocol interpretation of
last.inputTokens; the added measured-sequence documentation and end-to-end growth tests make the rollover semantics explicit and guard against reintroducing the prior defect. - Kill-list: scanned the new ranges — clean.
- CI:
gh pr checks 210→ color-guard, no-stub-gate, packages-linux, release-invariants, and app-macos all passing.
Findings
None.
Decision
The prior blocker is resolved at the new SHA with both positive aggregate-growth and anti-summed-input coverage. All live checks pass, so this PR is approved.
RE2 — APPROVE (at
|
project7-interns
left a comment
There was a problem hiding this comment.
Verdict: APPROVE
Epic Alignment: PASS
The corrective commit preserves the app-server contract and fixes the rollover evidence gap.
Checked (evidence)
- Delta
529b107..f016dba: whole-contextlast.inputTokenscomparison and rollover reset atthread/start(packages/engine/src/codex-app-server-engine.ts:476-507). - Aggregate-growth fixture and 5,000→6,200 crossing regression (
packages/engine/test/fake-app-server.mjs:35-45,108-130;packages/engine/test/codex-app-server-engine.e2e.test.ts:172-250). - Anti-summed-input regression (
packages/engine/test/codex-app-server-engine.e2e.test.ts:215-250). - Riskiest part: protocol context-token semantics; the measured-sequence documentation and spawned-process tests guard this behavior.
- Kill-list: new ranges scanned — clean.
- CI:
gh pr checks 210→ color-guard, no-stub-gate, packages-linux, release-invariants, and app-macos all passing.
Findings
None.
Decision
The prior blocker is resolved at f016dba; all live checks pass.
RE2 — APPROVE (compliant re-issue at
|
project7-interns
left a comment
There was a problem hiding this comment.
Verdict: APPROVE
Epic Alignment: PASS
The unchanged code remains aligned with #204; the required Design Fidelity documentation is now present and matches the implementation.
Checked (evidence)
- Design Fidelity table spot-checked: engine radiogroup and hidden Codex control (
src/settings-sheet.ts:81-91), gauge native detail/unknown fill (src/settings-sheet.ts:264-282), and accessibility pressed state (src/settings-sheet.ts:217-221). Claims match the livef016dbacode. - Coverage: table addresses layout/control placement, visibility, quota copy, gauge states, accessibility, and token/CSS reuse; no CSS files changed.
- Riskiest part: quota gauge semantics;
nativeDetailis used instead of meaningless USD fields, and unknown headroom renders no fill. - Kill-list: unchanged code ranges remain clean.
- CI: live
gh pr checks 210→ color-guard, no-stub-gate, packages-linux, release-invariants, and app-macos all passing.
Findings
None.
Decision
Design Fidelity is now documented and verified against the unchanged implementation. Approval stands at f016dba.
Fixes #204
Adds OpenAI's Codex CLI as a third translation tier, driving
codex app-serverover JSON-RPC/stdio. Gated on a measurement that Head required before any adapter code, and shipped under the PO's ruling on the residual risk.EPIC Alignment
eade8eb) — this consumes its seam and would not be shippable without it. Enables: none.codex app-serverJSON-RPC over stdio (neverexec); harness overhead measured first; the [refactor] Make the budget/fallback seam engine-agnostic — hours-remaining as the primitive, USD as only one derivation #205 headroom seam carries the budget decision; no credential or caption-content handling; the option is hidden without the binary.The gate came first
Head's instruction was to measure
codex app-serveroverhead at caption cadence and stop and report if it could not be reduced to an economical level — not build the adapter. Measured against the realcodex-cli 0.146.0:baseInstructions(translation prompt)sandbox:read-only,approvalPolicy:nevereffort:none,summary:none-62%. Worth noting the baseline row did not merely cost more — it did not do the job at all, so the prior ~15k figure was measuring a turn that produced no translation.
For comparison, LiveCap's Claude tier on the same probe and system prompt: 165 input tokens, 1.34 s. Codex at its best is ~31× the input tokens per turn, and that multiplier does not go away.
Why 31× was not the deciding number
A subscription user does not run out of tokens; they run out of quota. Measured directly via
account/rateLimits/read: 30 consecutive turns movedusedPercent18 → 18 — zero movement, 168,769 input tokens (145,664 cached), 55.8 s wall. That bounds it at <1% per 30 turns ⇒ ≥3,000 turns per weekly window ⇒ ≥4–7 hours of continuous captioning per week.The generalization caveat, verbatim from the gate report, as required:
Explicit limitations on that evidence: one account,
planType: "prolite", and the gauge is 1%-granular — so the hours figure is a bound derived from non-movement, not a measured rate. A plan that meters more tightly will buy fewer hours.The reason single-account evidence is nonetheless shippable (recorded so it is not re-derived): when headroom runs low the app falls back to Local automatically via the #205 seam, so on a tighter-metering plan the failure mode is graceful degradation to the free tier, not a broken session or a surprise bill.
Two measured facts that would otherwise have been bugs
Both came from driving the real binary rather than reading the schema:
secondaryisnullon this plan — one rate-limit window, not two. A design assuming two would have produced an unknown reading and silently disabled the safety net. The adapter handles 1..N windows.resetsAtis epoch seconds, while [refactor] Make the budget/fallback seam engine-agnostic — hours-remaining as the primitive, USD as only one derivation #205's seam documents epoch milliseconds. Unconverted,formatResetsIncomputesresetsAt - nowMswith no unit guard and renders "resets now" permanently — a string that reads as correct while being wrong. RE2 predicted this failure mode before review; there is now a test asserting the raw-vs-converted contrast directly.What is in the adapter
CodexAppServerEngine—thread/start→turn/startper turn,item/agentMessage/delta→ progressive snapshots,thread/resumefor crash recovery on the server-mintedthreadId(Codex mints it; the Claude path generates one).modelContextWindow. Per-turn input grows ~31 tokens/turn as the thread accumulates (5,168 → 6,088 over 30 turns, measured), so a long meeting walks toward the context wall. Codex exposes no cache signal comparable to the Claude tier'scacheReadInputTokens, so the trigger keys on the context window; it is flagged during a turn and applied before the next one, so an in-flight turn is never interrupted.CodexHeadroomSourcefeeding the [refactor] Make the budget/fallback seam engine-agnostic — hours-remaining as the primitive, USD as only one derivation #205 seam fromaccount/rateLimits/read.codexbinary is present, and a stale settings file selecting Codex on a machine without it falls back rather than leaving a hidden tier selected.Startup probe, honestly labelled.
app-serveris[experimental]with no stability promise, so scope 6 asked for a version pin plus a startup probe. The wire carries no protocol-version field —initializereturnsuserAgent/codexHomeonly — so there is nothing to compare a pin against. The probe is therefore functional:initializeandthread/startmust both succeed and yield a threadId before the engine reports ready. The constant documents what it can and cannot do rather than implying a handshake that does not exist.Cost, and the gauge
Codex exposes no USD anywhere (zero cost fields across the generated v2 schema).
Usage.turnCostUsd/cumulativeCostUsdare therefore 0 — not an invented figure — and the fallback decision rides entirely on the #205 headroom seam. No token→USD rate card is shipped.That has a display consequence RE2 caught, and it is a real bug rather than a cosmetic one: with a headroom source configured,
estimatedHoursRemainingcame from the seam butpoolUsd/spentUsd/fractionUsedstill came off the USD ledger — and the webview renders exactly those. A Codex user would have watched$0.00 / $20.00with an empty bar for an entire session, reading as budget untouched, while the real constraint sat unread innativeDetail. Fixed here: the gauge prefersnativeDetail(which on the Claude tier is the USD string, so both tiers take one path), the bar's fraction comes from the headroom source, and unknown headroom shows no fill rather than an empty bar implying a full allowance.On a Codex session the USD gauge fields are structurally meaningless, not merely zero — that engine reports no dollars at all. Nothing should render them, and after this PR nothing does.
Refresh cadence is a #205 seam property, not an adapter detail
Stated here at the operator's request so the next engine to use the seam does not rediscover it: #205 shipped
refreshHeadroom()without saying anything about how often it must be called. Priming it once — which is what this adapter did initially — means a multi-hour meeting decides the fallback on the quota reading taken at session start and never notices the allowance draining. That is a correctness bug in the seam's contract, surfaced by the first engine to consume it.This PR supplies the missing cadence (60 s tick, ticking only when a headroom source exists), but the obligation belongs to the seam: any future headroom source must be refreshed on a cadence, and the seam should say so.
What a failed refresh collapses to — fresh / stale / unknown
The operator asked which of the three states the window between a failed refresh and the next successful one collapses to. It collapses to
unknown, and deliberately not tofresh.refreshHeadroom()overwrites the cached reading with{known: false, reason: "unreadable"}on any failed read. Consequences, both intended:headroomKnown: false⇒ neitherisBelowThreshold()norevaluate()will auto-switch, per [refactor] Make the budget/fallback seam engine-agnostic — hours-remaining as the primitive, USD as only one derivation #205's rule that unknown is not "low".Treating stale as fresh was never on the table — that is the same class of bug as the priming gap above, one level down.
The residual weakness, stated rather than buried (RE2's point, and it is a fair one): collapsing stale → unknown means a sustained refresh failure disarms the auto-switch for the rest of the session. #205's "unknown never switches" rule was justified by transient failures, not sustained blindness. Two things bound the harm, neither of which makes it disappear:
account/rateLimits/readkeeps failing. If the app-server dies, turns throw and the loss-free router falls back to Local on its own, independently of headroom.RE2 proposed a better answer than mine: age the stale reading forward — a reading only ever decays, and the seam already has
ratePerHourplus metered time, sohoursFromRate(remaining − consumedSince, rate)keeps a bounded, conservative answer and degrades to unknown once the extrapolation hits zero or an explicit age bound is exceeded. I think that is right, and I am not doing it in this PR: it changes the decision semantics of a seam that is already merged and reviewed, on a PR that is already large, and it wants its own explicit, asserted age bound — precisely the kind of number that silently becomes wrong if it is smuggled in as an afterthought. Recommended as a follow-up ticket against #205 rather than a late addition here; happy to take it immediately if Head would rather it land now.Security
codexlogin. Detection is binary presence +--version; theDetectedCodexClitype carries no field that could hold login state.readRateLimitsdrops every identifying field at the boundary —planType,limitId, credit balances — so no account-identifying value travels further into the app. Asserted by a test that feeds them in and checks they cannot come out.read-onlywithapprovalPolicy: never, so no caption content can reach a shell or the filesystem. Asserted on real spawned argv.HeadroomUnknownReasonremains a closed literal set, so a source cannot smuggle an identifier into a gauge event or log line.Design Fidelity
The only UI surface is the Settings sheet: one new engine option, its note, and the gauge's amount/bar.
src/settings-sheet.tsis the sole changed UI file (+51/−2); no CSS file is touched at all (git diff --stat origin/main..f016dba -- '*.css'is empty), so every new element reuses existing tokens and classes..sh-seg-btninside the samerole="radiogroup" aria-label="Translation engine"as Claude CLI and Localsettings-sheet.ts:81,86hiddenin markup; revealed only whenhost_probereports acodexbinarysettings-sheet.ts:86,refreshEngines()cliwhen Codex is selected but absentrefreshEngines().sh-engine-notebeneath the control: own account, that plan's quota, measured 4–7 h/week, "less on a smaller one", never sees the login, falls back to Localsettings-sheet.ts:91nativeDetail; the Claude tier'snativeDetailis the"$x of $y"string, so both tiers take one path rather than the surface special-casing a tiersettings-sheet.ts:274headroomKnown === false⇒ no fill, with "usage unknown" as the amountsettings-sheet.ts:279.sh-seg,.sh-seg-btn,.sh-engine-note,.t-meta; zero CSS changes, no raw colorscolor-guardpassrole="radiogroup"+aria-label;aria-pressedmaintained per button inrenderControlssettings-sheet.ts:81textContent(#203)settings-sheet.tsNo screen in
design/screens/covers a third engine tier, so this follows the established pattern of that sheet rather than a reference image; the note sits where the existing engine note already sits.Self-Verification
Tests: engine 336 (+21 new), archive 110, app 185.
pnpm lint,pnpm typecheck(both configs),no-stub-gate,color-guardall clean.Nothing in the suite requires the
codexbinary to exist (#204 routing requirement). Afake-app-server.mjsreplays the REAL measured JSON-RPC shape over real process stdio — the same techniquefake-cli.mjsuses for the Claude tier — so the spawn → handshake → turn → notification path is exercised end-to-end, headless.Covered, each against that real spawned process:
app-serverin argv and neverexec, plus every one of the 29--disableflags — if that list is trimmed, the 62% saving silently evaporates, so it fails a test insteadthreadIdadopted; probed version parsedrecover()resumes onto the same thread id, and the next turn completesplanType/limitId/creditsnull(→ unknown, never infinite) rather than throwingClaude tier untouched: the #205 bit-for-bit oracle (96 pool/spend/metered combinations, exact
toBeequality) still passes, includingfractionUsedafter the gauge change.Kill-list: clean — no new dependency, no TODO/FIXME/stub marker, no caption content logged or persisted, no credential/token/plan-id reachable through any type in this path.
Deviations
DEFAULT_PERCENT_PER_HOUR/ rollover fraction are bootstrap constants, not a rate card. They play the same roledefaultDollarsPerHour = 0.4already did: a conservative value so the safety net is armed before enough metered time accrues. Any real measured rate replaces them.effort/summarylevers are deliberately not used. They produced no token saving and measurably worse cache reuse (3,456 → 1,408 cached), so the config stops at the levers that paid.codex-cli 0.146.0. The overhead figures, the nullsecondary, and the seconds-vs-ms unit are all from that one binary on one account; a different version may differ, which is why the startup probe is functional rather than a version comparison.