From 0d42ebcbbe5071341c4b8643ee15357c95a2ea06 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 4 Aug 2026 00:04:56 +0900 Subject: [PATCH 1/6] docs(triage): map the open bug surface and lock the stack order Sixteen bug issues, five of them unowned by any open PR. Records what the research round overturned: #907 cannot be fixed here because the canonical price source lives in lidge-jun/jawcode, and #875's reopen evidence tested a commit 115 before the fix that addresses it. --- .../260803_bug_backlog_stack/000_scope.md | 118 +++++++++++++++ .../010_disposition_sweep.md | 83 +++++++++++ .../020_long_context_pricing.md | 115 +++++++++++++++ .../030_cooldown_recovery.md | 135 ++++++++++++++++++ .../040_deepseek_residual.md | 96 +++++++++++++ .../050_jawcode_price_blocker.md | 93 ++++++++++++ 6 files changed, 640 insertions(+) create mode 100644 devlog/_plan/260803_bug_backlog_stack/000_scope.md create mode 100644 devlog/_plan/260803_bug_backlog_stack/010_disposition_sweep.md create mode 100644 devlog/_plan/260803_bug_backlog_stack/020_long_context_pricing.md create mode 100644 devlog/_plan/260803_bug_backlog_stack/030_cooldown_recovery.md create mode 100644 devlog/_plan/260803_bug_backlog_stack/040_deepseek_residual.md create mode 100644 devlog/_plan/260803_bug_backlog_stack/050_jawcode_price_blocker.md diff --git a/devlog/_plan/260803_bug_backlog_stack/000_scope.md b/devlog/_plan/260803_bug_backlog_stack/000_scope.md new file mode 100644 index 000000000..6f9499935 --- /dev/null +++ b/devlog/_plan/260803_bug_backlog_stack/000_scope.md @@ -0,0 +1,118 @@ +# 000 — Scope: sort the open issue surface, land the un-reviewed bugs as a stack + +## Objective + +Two deliverables, one unit. First, every open issue carries a disposition its +own content justifies. Second, the bugs that nobody has actually reviewed get +built — as a stacked pull-request chain, bottom-up, one layer per defect. + +Non-bug issues are label work only. Enhancements, roadmap items, and +upstream-blocked reports get their tags corrected and an evidence comment where +the disposition moved; they do not get code in this unit. + +## Baseline + +Measured 2026-08-03, `origin/dev` at `14b20def27f2d45f929c0dbb853fd0993ca61663`. + +- 39 open issues, 16 labeled `bug` +- 27 open pull requests + +The worktree HEAD is detached at `e835e789c` carrying an unrelated docs unit +(`260803_codex_desktop_toggle`). Every code claim below was read with +`git show origin/dev:`, not from the worktree. + +## The precedent this follows + +@Wibias ran #900→#905 as a five-layer stack in this repository: each pull +request targets the preceding stack branch, titles read `stack N/M`, and every +layer carries a stack-navigation comment listing the chain with "review and +merge bottom-up". The `enforce-target` check skips the wrong-base gate for +stacked children by design (`AGENTS.md`, Branch policy). That stack was closed +on the policy it encoded, not on its mechanics — the mechanics are the part +worth reusing. + +## Bug surface: who already owns what + +Sixteen `bug` issues. Most are already spoken for, and re-implementing them +would duplicate an open contributor pull request. + +| Issue | State | Owner | +|---|---|---| +| #586 | open PR | #935 @Wibias — Pool/Direct account mode switch | +| #893 | open PR | #928 @0xWinner98 — sparse Responses snapshot repair | +| #914, #919 | open PR | #922 @luvs01 — account-neutral network failures | +| #938 | open PR | #940 @mouzhi — UUID item-id normalization | +| #92, #241, #417 | upstream tracker | Codex CLI/Desktop, kept open for discoverability | +| #418, #796, #904 | awaiting reporter | a named capture would settle each | +| #907, #908, #915, #545, #875 | **unowned** | this unit | + +The five unowned ones are the implementation surface. Everything else is a +label or a comment. + +## What the research round overturned + +**#907 cannot be fixed in this repository.** The working assumption was a +metadata regeneration. It is not: `scripts/generate-jawcode-metadata.ts:22-24` +reads `../jawcode/packages/ai/src/models.json`, and that canonical source +carries the same stale numbers — Terra `2.5/15/0.25/3.125` and Luna +`1/6/0.1/1.25` — across four provider bundles (`openai`, `openai-codex`, +`github-copilot`, `opencode-zen`) in a different repository, +`lidge-jun/jawcode`. Regenerating today reproduces the defect exactly. + +The overlay is not an escape hatch either. `src/usage/expected-prices.ts:1-11` +scopes it to models whose jawcode rows are missing or all-zero, and +`src/usage/cost.ts:139-145` gives a valid nonzero jawcode row precedence over +it. A nonzero-but-wrong row is never reached by the overlay. Hand-editing the +generated file contradicts its own header (`src/generated/jawcode-model-metadata.ts:1-2`) +and would be silently reverted by the next regeneration — while +`tests/jawcode-metadata-sync.test.ts:21-47` byte-compares against the same +stale source, so it would fail. + +**The reporter's cache-write claim is wrong, and so was one of ours.** The +report asserted cache writes should be zero. The official page publishes a +`Short context cache writes` column with nonzero values for all three models. + +**#908 and #907 are independent and must not be bundled.** #908 is a missing +*multiplier* stage; #907 is a wrong *base* rate. Fixing #908 does not correct +Terra/Luna absolute estimates, and #907 landing would not add tier selection. +They touch the same file and are still two changes. + +## Verified pricing (Tier 2, opened directly) + +`agbrowse fetch "https://developers.openai.com/api/docs/pricing.md" --json +--browser never` → `verdict=strong_ok`, retrieved 2026-08-03. The agbrowse +endpoint resolver misroutes the HTML URL to `rss.xml`; the `.md` representation +is the one that proves. + +Published table, USD per 1M tokens, `input / cachedInput / cacheWrite / output`: + +| Model | Short context | Long context | +|---|---|---| +| `gpt-5.6-sol` | 5.00 / 0.50 / 6.25 / 30.00 | 10.00 / 1.00 / 12.50 / 45.00 | +| `gpt-5.6-terra` | 2.00 / 0.20 / 2.50 / 12.00 | 4.00 / 0.40 / 5.00 / 18.00 | +| `gpt-5.6-luna` | 0.20 / 0.02 / 0.25 / 1.20 | 0.40 / 0.04 / 0.50 / 1.80 | + +Long context is exactly 2× input, 2× cached input, 2× cache write, 1.5× output, +applied to the whole request past `> 272,000` input tokens. Sol's short rates +match the bundle; Terra and Luna do not — which is #907, measured rather than +recalled. + +Other tiers, same round: xAI Grok 4.5 at `>= 200,000` (inclusive, 2× on every +rate) and MiniMax M3 at `> 512,000`. + +## Work-phase map + +Dependency order. The stack is built bottom-up so each layer's diff is readable +on its own. + +| Phase | Doc | Unit | Depends on | +|---|---|---|---| +| 1 | `010` | Disposition sweep: labels + evidence comments | — | +| 2 | `020` | #908 long-context pricing tiers | — | +| 3 | `030` | #915 cooldown early-recovery probe | — | +| 4 | `040` | #545 and #875 — code or evidenced disposition | research | +| — | `050` | #907 blocker record (no code lands here) | — | + +#907 has no implementation phase on purpose. Its deliverable is the proof that +the fix belongs in `lidge-jun/jawcode`, plus the regression test that becomes +possible once the source moves. diff --git a/devlog/_plan/260803_bug_backlog_stack/010_disposition_sweep.md b/devlog/_plan/260803_bug_backlog_stack/010_disposition_sweep.md new file mode 100644 index 000000000..c7550c18c --- /dev/null +++ b/devlog/_plan/260803_bug_backlog_stack/010_disposition_sweep.md @@ -0,0 +1,83 @@ +# 010 — Phase 1: disposition sweep + +Label work only. No code lands in this phase. + +## Method + +Every open issue was read with its full comment history and compared against the +repository's live label semantics. A change is recommended only where the +issue's own content or a maintainer comment justifies it. + +## Changes to apply + +### Awaiting reporter → `needs-info` + +Five issues have a maintainer comment naming a specific capture that would +settle them, but no label saying so. Without the label they read as unowned +work. + +| Issue | The capture that would settle it | Age | +|---|---|---| +| #904 | failing `OCX_LIVE_FRAME_LOG` capture of the Korean corruption | ~20h | +| #796 | live Volcengine Ark result + regional hostname + redacted error shape | ~3.5d | +| #695 | exact switch triggers, affinity rules, unknown-quota behavior | ~5.2d | +| #561 | four concrete provider-evidence items for the Modelsell preset | ~7d | +| #418 | one current custom-parent → custom-child three-boundary trace | ~10.1d | + +None warrants `stale` yet — each has recent substantive activity or a fresh +maintainer request. `stale` is for silence, not for age. + +### #919 is not a bug + +`bug` → `enhancement`. The maintainer confirmed the routing effect is real but +recorded that it was introduced deliberately: +`devlog/_fin/260722_issue_bug_sweep/030_patch_s_sticky_502.md` states the +expected outcome as `transient 실패 기록, affinity 해제` so account health treats +a mid-stream reset as transient. Reversing it is a policy decision, so the +issue is a behavior-change request. It keeps `proxy`, `streaming`, `tools`. + +### Accepted long-term work → `roadmap` + +`#820`, `#657`, `#656`, `#572`. Each has a maintainer comment accepting the +direction while splitting delivery across multiple phases or PRs — which is +exactly what `roadmap` means in this repository. + +### Missing compatibility and area labels + +| Issue | Add | Why | +|---|---|---| +| #938 | `provider-compatibility`, `provider` | non-canonical provider item IDs, called a compatibility defect by the reviewer | +| #893 | `provider-compatibility`, `provider` | opt-in repair for sparse gateway snapshots | +| #875 | `provider-compatibility`, `provider`, `streaming` | DeepSeek-specific lifecycle defect after a successful stream | +| #796 | `provider-compatibility` | Ark-specific, alongside `needs-info` | +| #586 | `gui` | the backend exists; the defect is entirely a missing dashboard control | +| #806 | `gui`, `cli` | the correction spans dashboard copy, CLI text, and docs | +| #92 | `tools` | the blocked flow is cross-provider sub-agent delegation | +| #425 | `catalog` | account-qualified namespaces change catalog generation | +| #414, #415 | `provider` | both evaluate external search providers | +| #177, #178 | `platform`, `tools` | Warp/Factory need agent-execution backends, not model presets | +| #95 | `platform`, `proxy` | multi-user hosting, tenant isolation, authorization | + +### Maintainer-sponsored surfaces + +`#656` and `#386` change auth lifecycle and release packaging respectively; +both have maintainer sponsorship on record. `#809` gets `maintainer-sponsored` +and loses `streaming` — it is an authentication route split and has nothing to +do with stream processing. + +## Deliberately unchanged + +`#92`, `#241`, `#417` keep `upstream-tracking`. They are the only three that +meet the definition: blocked on a Codex CLI/Desktop fix. Others block on +provider vendors (#540 on Automattic, #201 on a sanctioned contract) or on +source data, which is a different kind of external and must not be conflated. + +`#908` keeps `bug` + `gui`. The reviewer already re-classified it from a GUI +enhancement to a real cost-estimation defect and kept `gui` as the visible +surface. + +## No closures + +Zero issues qualify for closure. Several carry landed partial fixes — #796, +#875, #904, #545 — but each has an unverified or still-reproducing residual. +Closing on a partial fix is how a defect gets buried. diff --git a/devlog/_plan/260803_bug_backlog_stack/020_long_context_pricing.md b/devlog/_plan/260803_bug_backlog_stack/020_long_context_pricing.md new file mode 100644 index 000000000..595f6406f --- /dev/null +++ b/devlog/_plan/260803_bug_backlog_stack/020_long_context_pricing.md @@ -0,0 +1,115 @@ +# 020 — Phase 2: #908 long-context pricing tiers + +Stack layer 1. Work class C2: one conventional slice through an existing +pricing pipeline, no new module boundary. + +## The defect + +Several vendors reprice the **entire request** once the prompt crosses a token +threshold. The estimator cannot express that. `resolveMatchedPrice()` resolves +one flat `Cost4` and never sees a token count, so every request bills at the +short rate — including the long ones, which are the expensive ones. + +`applyPriorityMultiplier()` (`src/usage/cost.ts:269-286`) already establishes +the shape of a conditional repricing step, but it keys off `serviceTier`, so it +cannot be reused as-is. + +## The trap + +The threshold is measured on **raw** `usage.inputTokens` — the total prompt +size including cache reads and writes (`src/types.ts:311-318`). But +`normalizeCostTokens()` subtracts cache read and write to produce billable +input (`src/usage/cost.ts:121`). A 280k prompt with a 200k cache read has 80k +billable input and still crosses the 272k threshold. + +Selecting the tier after normalization would silently under-bill exactly the +cache-heavy long requests — the ones where being right is worth the most. Tier +selection reads `usage.inputTokens` directly; `tokens.input` is forbidden here. + +## Verified thresholds + +| Model | Threshold | Operator | Multiplier `in/out/cRead/cWrite` | +|---|---|---|---| +| `gpt-5.6-sol`, `-terra`, `-luna` | 272,000 | `>` | 2 / 1.5 / 2 / 2 | +| `grok-4.5` | 200,000 | `>=` | 2 / 2 / 2 / 2 | +| `MiniMax-M3` | 512,000 | `>` | 2 / 2 / 2 / 2 | + +The OpenAI operator is exclusive: the page reads "Prompts with >272K input +tokens". xAI's is inclusive: "Long context ≥ 200k tokens". Getting these +backwards is a one-token error nobody would ever notice, so both boundaries get +a test. + +## Model-id exactness + +`src/generated/jawcode-model-metadata.ts:44` contains **both** `minimax-m3` +(0.6/2.4/0.12/0) and `MiniMax-M3` (0.3/1.2/0.06/0). The first-party registry ID +is the cased `MiniMax-M3` (`src/providers/registry.ts:247-255`). Case-folding +the lookup would select the wrong base row — the tier rule must match exactly. + +Provider scoping matters for the same reason: `cursor` and `openrouter` routes +resell these models under their own terms and must not be charged first-party +tier rules. + +## Design + +`src/usage/expected-prices.ts` — add beside `Cost4`: + +```ts +export type ContextTierName = "long"; + +export interface ContextTier { + thresholdInputTokens: number; + inclusive: boolean; + multiplier: Cost4; + source: string; + verifiedAt: string; +} +``` + +plus an exactly-keyed `CONTEXT_TIERS` registry (`${provider}\0${modelId}`) and +`findContextTier()` / `isLongContext()`. No fuzzy matching, no case folding, no +model-level fallback. Every row records its official URL and `verifiedAt`. + +`src/usage/cost.ts` — add `applyContextTier()` and insert one stage: + +```text +base price → context multiplier → Fast multiplier → calculateCost +``` + +`resolveMatchedPrice()` stays token-independent; it is memoized by +provider/model (`src/usage/cost.ts:153-162`) and passing a token count would +poison that cache. The tier is selected after price resolution and before +`calculateCost()`, in both `estimateAttemptCost()` and `estimateRequestCost()`. + +`contextTier?: ContextTierName` is added to `AttemptCostEstimate` and +`CostEstimate` so the dashboard can distinguish "long" from "just a bigger +number". A combo result carries it when any attempt does, while each attempt +keeps its own. + +## Worked example + +Sol, 300,000 input + 20,000 output, no cache: + +- short: `300000/1e6 × 5 + 20000/1e6 × 30` = `1.50 + 0.60` = **$2.10** +- long: `300000/1e6 × 10 + 20000/1e6 × 45` = `3.00 + 0.90` = **$3.90** +- long + Fast: **$7.80** + +## Tests + +Extend `tests/usage-cost.test.ts` — the sibling already covering normalization, +resolution, attempts, combos, and Fast composition. + +1. Each OpenAI model at exactly 272,000 (no tier) and 272,001 (tier). +2. Grok at 199,999 (no tier) and exactly 200,000 (tier) — inclusive boundary. +3. `MiniMax-M3` at 512,000 / 512,001; lowercase `minimax-m3` gets no tier. +4. **Raw-vs-normalized**: raw input above 272k with a cache read large enough + that normalized input falls below it — tier must still activate. +5. `cursor/gpt-5.6-sol` and `openrouter/openai/gpt-5.6-sol` stay untiered. +6. An untiered model is unchanged above every threshold. +7. The $2.10 / $3.90 worked example. +8. Fast composition → $7.80. +9. Combo propagation: one long attempt + one standard attempt. + +The existing Fast fixture at `tests/usage-cost.test.ts:408` uses 1M input, +which now crosses the threshold. It must move below 272k or its expected totals +change — the kind of silent fixture breakage that looks like a regression. diff --git a/devlog/_plan/260803_bug_backlog_stack/030_cooldown_recovery.md b/devlog/_plan/260803_bug_backlog_stack/030_cooldown_recovery.md new file mode 100644 index 000000000..04aab99eb --- /dev/null +++ b/devlog/_plan/260803_bug_backlog_stack/030_cooldown_recovery.md @@ -0,0 +1,135 @@ +# 030 — Phase 3: #915 cooldown early-recovery probe + +Stack layer 2. Work class C3: crosses routing, auth resolution, quota parsing, +and the state sweeper. Account-pool state, so the audit gate applies. + +## The defect, precisely + +A reset-derived cooldown is a *prediction*. OpenAI can reset earlier than +predicted, and when it does, the cooled account should come back. It does not, +because the probe that would notice is gated behind a selection that will never +happen. + +The chain, verified on `origin/dev`: + +1. Cooled accounts are filtered out **before** every strategy runs + (`src/codex/routing.ts:740-768`). All three selectors consume only the + already-filtered list (`:869-905`, `:914-972`). +2. Probe eligibility exists and is correct — non-`retry-after` cooldowns, one + lease per interval, generation-fenced (`:404-412`, `:459-471`). +3. But `resolveCodexAuthContext()` **selects the account first** + (`src/codex/auth-context.ts:238-247`) and only then checks that account's + lease (`:276-298`). + +So with B eligible, A is never selected, never reaches the lease code, and +never gets probed. Selection-before-probe. The recovery mechanism is not +broken; it is unreachable. + +A forced WHAM read does not help either: `setAccountQuotaFromParsed()` +(`src/codex/auth-api.ts:614-623`) writes the quota cache +(`src/codex/quota.ts:134-179`) and never touches routing state. + +## Two corrections to the prior analysis + +**The existing predicate is too permissive for this use.** It excludes only +`retry-after`, so it admits both `reset-derived` and `default` cooldowns +(`src/codex/routing.ts:127`, `:404-412`). A `default` cooldown is the 60-second +fallback for a 429 with no headers at all — there is no prediction to be early +against. The background worker must be strictly narrower than the request path: +`cooldownSource === "reset-derived"` exactly. + +**`clearCodexAccountCooldown()` must not be used.** It clears account-wide +health and then iterates every scoped entry (`src/codex/routing.ts:579-614`), +and takes no generation argument. Recovering `shared` would silently clear +`spark`. + +## Fences that must hold + +Three independent generations already exist and all three matter here: + +- **cooldown generation** — a newer 429 during the probe must not be erased by + the older probe's result. +- **credential generation** — a pool credential replaced mid-probe invalidates + the result (`src/codex/account-store.ts:131-145`). +- **main-account identity** — the main account has no numeric generation; its + fence is the physical ChatGPT account ID (`src/codex/auth-collision.ts:70-74`). + +## Design + +`src/codex/routing.ts` — add `claimDueCodexQuotaRecoveryProbes(config, limit, +now)` and `settleCodexQuotaRecoveryProbe(claim, recovered, proof, now)`. The +claim enumerates accounts **independently of strategy selection**, requires +`reset-derived`, reuses the existing interval/in-flight checks, and captures +lease id + cooldown generation + scope + credential fence atomically in one +synchronous turn. At most one scope per account per pass. + +Settle clears **only** the exact map entry, and only when every fence still +matches. Any mismatch — stale credential, replaced cooldown, incomplete +snapshot, failed fetch — releases the lease and **retains** the cooldown. + +Recovery must not route through `recordCodexUpstreamOutcome(200)`: success +handling also mutates account-wide failure state (`:1261-1301`). A background +observation is not a request outcome and must not be laundered into one. + +`src/codex/quota.ts` — add `isCompleteCodexQuotaRecoverySnapshot()`. WHAM can +return a credits-only payload with no usage windows at all +(`src/codex/quota.ts:360-410`); treating that as recovery would clear a +cooldown on no evidence. Go/Free require a finite `monthlyPercent`, other plans +a finite `weeklyPercent`, and recovery additionally requires not-exhausted. + +`src/codex/auth-api.ts` — add `runCodexCooldownRecoveryProbes()`, coalesced by +a module-level promise, bounded by the existing `mapWithConcurrency(..., 4, +...)`, joining the existing per-account single-flight rather than opening a +parallel one. Registered on the state sweeper's existing 60s tick +(`src/lib/state-store-sweeper.ts:155-166`) — no new timer. + +The worker never pauses an account, changes the active account, clears +affinity, or synthesizes an upstream outcome. + +`src/codex/auth-context.ts` — unchanged. Background recovery removes the +selection dependency without touching request admission. + +## The Spark honesty constraint + +`GET /backend-api/wham/usage` takes no model or scope parameter +(`src/codex/auth-api.ts:589-592`) and returns generic weekly/monthly windows +(`src/codex/quota.ts:29-47`). So a generic WHAM result cannot be proven +authoritative for the `spark` scope. + +The claim/settle layer fences scopes exactly, and a Spark cooldown is +**retained** when the snapshot cannot be proven to describe Spark. Under- +recovering is a delay; over-recovering sends traffic to an account that is +still restricted. + +## Tests + +New `tests/codex-cooldown-recovery.test.ts`; routing contract tests stay in +`tests/codex-routing.test.ts`. + +The red-green case that defines this phase: A cooled reset-derived, B eligible, +ordinary routing selects B, worker runs after the interval — assert WHAM +receives **A's** credential, A's matching cooldown clears, and no routing call +ever selected A. That is the whole defect in one test. + +Then the retention cases: still-100% WHAM, credits-only/windowless snapshot, +non-2xx, timeout, parse failure, admission-busy — each retains. The race cases: +credential replaced mid-probe, newer 429 mid-probe, concurrent worker passes +collapsing to one WHAM. The scope cases: shared recovery leaves Spark cooled, +and the reverse. The never-claimed cases: `retry-after` and `default` cooldowns +produce no WHAM request at all. + +One assertion to **not** write: "a 100% snapshot never rebinds an existing +thread." Quota strategy deliberately rebinds an over-threshold bound thread +(`src/codex/routing.ts:1179-1205`) while fill-first and round-robin preserve +affinity (`:1186-1188`). Asserting otherwise would encode a policy change this +phase is not making — the maintainer scoped it out in +`devlog/_plan/260803_cooldown_recovery_probe/000_plan.md:41-43`. + +## PR #922 overlap + +#922 touches neither `routing.ts`, `auth-api.ts`, nor `quota.ts`, so it does +not supersede this. It does add a sidecar `releaseProbeLease` finalizer and +releases request leases on account-neutral transport failures. Sharing the same +lease fields makes a stale claim a harmless no-op. The only file both touch is +`src/server/index.ts`, in unrelated hunks. #922 currently has changes +requested, so this phase must not import its classifier. diff --git a/devlog/_plan/260803_bug_backlog_stack/040_deepseek_residual.md b/devlog/_plan/260803_bug_backlog_stack/040_deepseek_residual.md new file mode 100644 index 000000000..0269a9272 --- /dev/null +++ b/devlog/_plan/260803_bug_backlog_stack/040_deepseek_residual.md @@ -0,0 +1,96 @@ +# 040 — Phase 4: #875 DeepSeek tool-loop residual + +Outcome: **no code**. The reopen evidence does not describe current `dev`. + +## What the issue claims + +#875 was closed as fixed by #892, then reopened on 2026-08-03: the reporter +tested a candidate branch, the initial DeepSeek Responses request returned 200 +with output and reasoning tokens, no `function_call_output` continuation was +ever sent, and Codex Desktop stayed pending. + +That is a serious report and the reopen was the right call on the information +available. The information turned out to be stale. + +## The ancestry check + +The reporter tested commit `0b30283b6`. The commit that fixes DeepSeek +Responses over the Codex WebSocket is `5dd965a13` — "fix DeepSeek Responses +over Codex WebSocket". + +```console +$ git merge-base --is-ancestor 5dd965a13 origin/dev && echo YES +YES +$ git merge-base --is-ancestor 0b30283b6 5dd965a13 && echo "tested BEFORE the fix" +tested BEFORE the fix +$ git rev-list --count 0b30283b6..5dd965a13 +115 +``` + +The tested build predates the fix by 115 commits. On `0b30283b6`, DeepSeek WS +turns still went through upstream SSE, where a missing or delayed terminal +leaves the client waiting — which is exactly the reported symptom. The +reproduction is real; it just reproduces a bug that has since been fixed. + +## What current dev actually emits + +A Codex Desktop turn against DeepSeek takes the bounded-JSON path, not SSE. The +registry declares `modelWebsocketUpstreamStreaming: false` +(`src/providers/registry.ts:1077-1080`), final-route normalization switches the +upstream request to `stream:false` +(`src/server/responses/core.ts:818-834`), and the JSON response is handed to +the WS bridge (`src/server/index.ts:1048-1057`). + +`sendResponsesJsonAsEvents()` (`src/server/ws-bridge.ts:289-324`) then emits: + +1. `response.created` +2. one `response.output_item.done` per `output[]` item — **including + `function_call` items**, since the loop is untyped and forwards every item +3. `response.completed` / `.failed` / `.incomplete` + +That is the sequence Codex needs. Its WebSocket frames enter the same parser as +SSE, and that parser accepts a function call directly from +`response.output_item.done` — `output_item.added` and +`function_call_arguments.done` are not prerequisites. On the done item Codex +queues the tool and sets `needs_follow_up`; on `response.completed` it drains +and issues the next `response.create` carrying `function_call_output`. + +So there is no missing frame to fix on current `dev`. + +## #940 does not touch this path + +@mouzhi's #940 is real work for #938 and now also enables the repair on the +built-in DeepSeek preset. But every repair integration sits under the +`isEventStream` branch (`src/server/responses/core.ts:1851`), and DeepSeek WS +turns reach the JSON branch (`:2007`). The PR itself says JSON repair is not +implemented. + +@Ingwannu's path distinction was therefore correct even as the PR moved. #940 +should land or not land on #938's merits; it neither fixes nor blocks #875. + +## DeepSeek's documented contract + +Retrieved 2026-08-03 from the official +[Responses API reference](https://api-docs.deepseek.com/api/create-response/) +and [compatibility guide](https://api-docs.deepseek.com/guides/responses_api/): +the API is stateless, `previous_response_id` is unsupported so clients replay +full history, `function_call` / `function_call_output` are supported input +items, `call_id` must be non-empty and unique, and plain-text reasoning is +merged into the adjacent assistant message. + +Nothing there contradicts what ocx emits. + +## Disposition + +Keep the issue open — a reporter saw a real stall and deserves confirmation, +not a close on an ancestry argument they cannot check themselves. The comment +states the ancestry finding, names `5dd965a13`, and asks for a re-test on a +build containing it. + +If it still reproduces, the capture that settles it: proof the build contains +`5dd965a13`, the raw DeepSeek JSON, the exact ocx→Codex WS frames (especially +the `response.output_item.done` function item and the terminal frame), that +item's `type`/`call_id`/`name`/`arguments`, and whether a second inbound +`response.create` reached ocx at all. That last one splits the remaining space +cleanly: if it arrives, the defect is downstream of the handoff; if not, it is +the client's move that never came. diff --git a/devlog/_plan/260803_bug_backlog_stack/050_jawcode_price_blocker.md b/devlog/_plan/260803_bug_backlog_stack/050_jawcode_price_blocker.md new file mode 100644 index 000000000..53056fc95 --- /dev/null +++ b/devlog/_plan/260803_bug_backlog_stack/050_jawcode_price_blocker.md @@ -0,0 +1,93 @@ +# 050 — #907 stale bundled prices: the blocker record + +Outcome: **no code in this repository**. The fix belongs in +`lidge-jun/jawcode`. + +## Why this is not a regeneration + +The obvious move is `bun run generate:jawcode-metadata` and commit the result. +That reproduces the defect exactly. + +`scripts/generate-jawcode-metadata.ts:22-24` resolves its source from +`JAWCODE_MODELS_JSON` or `../jawcode/packages/ai/src/models.json`, and +`:82-98` copies the four cost fields with no transformation. The generated file +is a pure projection. And the canonical source carries the same stale numbers: + +```console +$ python3 - <<'EOF' # /Users/jun/Developer/new/700_projects/jawcode +/openai/gpt-5.6-terra {'input': 2.5, 'output': 15, 'cacheRead': 0.25, 'cacheWrite': 3.125} +/openai/gpt-5.6-luna {'input': 1, 'output': 6, 'cacheRead': 0.1, 'cacheWrite': 1.25} +/openai-codex/gpt-5.6-terra {'input': 2.5, ...} +/github-copilot/gpt-5.6-terra{'input': 2.5, ...} +/opencode-zen/gpt-5.6-terra {'input': 2.5, ...} +EOF +``` + +Four provider bundles, same wrong numbers, different repository. + +## Why the overlay is not an escape hatch + +`src/usage/expected-prices.ts:1-11` scopes the overlay to models whose jawcode +rows are **missing or all-zero**, and `src/usage/cost.ts:139-145` gives a valid +nonzero jawcode row precedence over it. A nonzero-but-wrong row is never +reached. Zero means "not billable here", not "free" — that distinction is the +whole design, and widening it to mean "or wrong" would break the precedence +contract for every model. + +## Why hand-editing the bundle is not an option + +`src/generated/jawcode-model-metadata.ts:1-2` declares itself generated. The +next regeneration reverts the edit silently, and +`tests/jawcode-metadata-sync.test.ts:21-47` regenerates from the same source +and byte-compares — so a hand-edit fails CI immediately, which is the test +working correctly. + +Passing a corrected JSON through `JAWCODE_MODELS_JSON` would produce the right +bytes while canonical jawcode stays wrong. That is deliberate source +divergence dressed up as a fix. + +## The correction to the report + +The reporter asserted cache writes should be zero. They should not. The +official page publishes a `Short context cache writes` column with nonzero +values for all three models (retrieved 2026-08-03, `verdict=strong_ok`): + +| Model | input / cachedInput / cacheWrite / output | +|---|---| +| `gpt-5.6-sol` | 5.00 / 0.50 / 6.25 / 30.00 | +| `gpt-5.6-terra` | 2.00 / 0.20 / 2.50 / 12.00 | +| `gpt-5.6-luna` | 0.20 / 0.02 / 0.25 / 1.20 | + +Sol's bundled row is already correct. A regeneration that zeroed cache writes +would replace one wrong number with another — and it would have looked like +progress. + +## The upstream change + +In `lidge-jun/jawcode`, `packages/ai/src/models.json`: + +```diff + "gpt-5.6-terra": { "cost": { +- "input": 2.5, "output": 15, "cacheRead": 0.25, "cacheWrite": 3.125 ++ "input": 2, "output": 12, "cacheRead": 0.2, "cacheWrite": 2.5 + }} + "gpt-5.6-luna": { "cost": { +- "input": 1, "output": 6, "cacheRead": 0.1, "cacheWrite": 1.25 ++ "input": 0.2, "output": 1.2, "cacheRead": 0.02, "cacheWrite": 0.25 + }} +``` + +Sol unchanged. All four provider bundles need the same correction or they will +contradict each other. + +## What lands here afterwards + +Regenerate, commit the generated diff without hand-editing, and add a +table-driven regression near `tests/usage-cost.test.ts:153-159` asserting +`resolveMatchedPrice("openai", model)` returns the exact tuple with +`source: "jawcode"` and `status: "verified"` for all three models. + +That test is the durable part. The existing sync test proves the bundle matches +its source; it cannot prove the source matches the vendor. This one pins the +actual published numbers, so the next price cut fails a test instead of +silently overcharging users. From 5fc62c8fcde8b67fdca683216ec32367dc0c69b9 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 4 Aug 2026 00:14:15 +0900 Subject: [PATCH 2/6] docs(triage): #545 is a thinking round-trip loss, not an identity-block budget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The standing hypothesis said the prepended OAuth identity consumed the classifier's 64 output tokens. A system prompt is input; it cannot. The real loss is that thinking:{type:disabled} translates to reasoning:undefined, which is indistinguishable from omitted — and omitted means adaptive thinking ON. --- .../060_classifier_thinking.md | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 devlog/_plan/260803_bug_backlog_stack/060_classifier_thinking.md diff --git a/devlog/_plan/260803_bug_backlog_stack/060_classifier_thinking.md b/devlog/_plan/260803_bug_backlog_stack/060_classifier_thinking.md new file mode 100644 index 000000000..e8c733c90 --- /dev/null +++ b/devlog/_plan/260803_bug_backlog_stack/060_classifier_thinking.md @@ -0,0 +1,129 @@ +# 060 — #545 Claude Desktop classifier retries + +Stack layer 3. Work class C4 by promotion: the change lands on an Anthropic +OAuth execution path, so `MAINTAINERS.md` security review applies even though +no credential handling is touched. + +## The reported symptom + +In Claude Desktop 3P gateway-key mode with an Anthropic OAuth provider, Claude +Code's Auto Mode permission classifier is truncated at exactly 64 output tokens +with `max_tokens` and retries up to five times. The reporter's own aggregate: +1,084 truncated requests against 143 that completed under 64 tokens. Every tool +approval costs 12–22 seconds. + +## The standing hypothesis was wrong + +The recorded analysis said the classifier's 64-token budget "has to accommodate +the prepended identity block, and that is where the retries come from." + +That cannot be the mechanism. The identity block is prepended to the **system** +prompt (`src/adapters/anthropic.ts:752-757`), and `max_tokens` caps **output**. +Input and output are different budgets; a longer system prompt cannot arithmetically +consume output allowance. + +This repository had already reached that conclusion once and forgotten it: +`devlog/_fin/260728_bug_bundle_resolution/030_claude_system_dedup.md` abandoned +an identity-dedup patch for exactly this reason. Re-deriving a rejected theory +is what a devlog is supposed to prevent. + +## The actual mechanism + +A round-trip fidelity loss, three hops long. + +**Hop 1 — the client says "no thinking".** The reporter's inbound capture +carries `thinking: {type: "disabled"}` alongside `max_tokens: 64` and +`stop_sequences: [""]`. + +**Hop 2 — ocx drops the instruction.** `src/claude/inbound.ts:494-506` treats +disabled thinking as *nothing to translate*: + +```ts +const thinkingDisabled = isRec(thinking) && thinking.type === "disabled"; +if (!thinkingDisabled && (isRec(thinking) || outputConfigEffort !== undefined)) { + ... + body.reasoning = reasoning; +} +``` + +`reasoning` stays `undefined` — the same state produced by a request that never +mentioned thinking at all. The existing test pins this +(`tests/claude-inbound.test.ts:96-97`): both "disabled" and "omitted" assert +`reasoning` is `undefined`. Two different intentions, one representation. + +**Hop 3 — omission means the opposite upstream.** The adapter emits `thinking` +only for a real non-`none` effort (`src/adapters/anthropic.ts:769-770`), so the +outbound request omits the field. Anthropic documents that for Sonnet 5 an +omitted `thinking` field means *adaptive thinking is on by default*, and that +thinking tokens count against `max_tokens` (retrieved 2026-08-04: +[what's new in Sonnet 5](https://platform.claude.com/docs/en/about-claude/models/whats-new-sonnet-5), +[extended thinking](https://platform.claude.com/docs/en/build-with-claude/extended-thinking)). + +So the model thinks, thinking eats the 64-token budget, and generation stops +before `` is ever emitted. The classifier never sees its stop sequence, +Claude Code retries, and the loop runs to its five-attempt ceiling. The 64 +tokens is the caller's own value and is faithfully preserved +(`src/adapters/anthropic.ts:750`) — that part of the prior analysis was right. + +The client asked for no thinking. It got thinking. That is the whole bug. + +## Design + +Give "disabled" a representation that survives the trip, using the sentinel the +parser already understands — `"none"` is documented at +`src/adapters/anthropic.ts:766-769` as the disable sentinel. + +`src/claude/inbound.ts`: + +```diff +-if (!thinkingDisabled && (isRec(thinking) || outputConfigEffort !== undefined)) { ++if (thinkingDisabled) { ++ body.reasoning = { effort: "none", summary: "none" }; ++} else if (isRec(thinking) || outputConfigEffort !== undefined) { +``` + +`src/adapters/anthropic.ts` — emit the explicit disable before the existing +non-`none` branch, gated on models where omission enables thinking: + +```diff ++if (parsed.options.reasoning === "none" && usesAdaptiveThinking(parsed.modelId)) { ++ body.thinking = { type: "disabled" }; ++} else + if (typeof parsed.options.reasoning === "string" && parsed.options.reasoning !== "none") { +``` + +**Pre-write search (DEV-NECESSITY-01).** No new predicate is needed. +`usesAdaptiveThinking()` already exists at `src/adapters/anthropic.ts:411-421` +and version-gates precisely the families whose omission-default this depends on, +including date-pinned and suffixed ids. Adding a parallel model matcher would +duplicate a subtle regex that is already correct and already tested. + +Explicitly unchanged: `max_tokens`, `stop_sequences`, the OAuth identity block, +terminal mapping, retry behavior, native passthrough. + +## Tests + +`tests/claude-inbound.test.ts:96` — the pinned expectation changes from +`undefined` to `{effort: "none", summary: "none"}`. Line 97 (omitted thinking → +`undefined`) must **stay** `undefined`: that assertion is what proves disabled +and omitted are no longer the same state, which is the entire fix. + +`tests/anthropic-reasoning.test.ts` — adaptive model + `"none"` emits +`thinking: {type: "disabled"}`; adaptive model with reasoning omitted still +omits `thinking`; a non-adaptive model + `"none"` emits nothing; existing +enabled/adaptive paths unchanged. + +Round-trip regression — the reporter's exact shape in, and assert the final +adapter body carries `max_tokens: 64`, `stop_sequences: [""]`, +`thinking: {type: "disabled"}`, and the OAuth identity still first in `system`. + +## Honest limits + +The unit tests prove the wire shape, not the retry disappearing. Confirming +that requires a live Claude Desktop 3P + Anthropic OAuth session showing the +classifier terminating on `` instead of `max_tokens`. The PR says so +rather than claiming the symptom fixed. + +No public Anthropic documentation was found specifying the exact Claude Code +OAuth identity string requirement; that remains repository knowledge from live +behavior. The fix leaves it untouched, so nothing depends on proving it here. From 71e14191654591068b40a79549620759d50c481e Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 4 Aug 2026 00:17:38 +0900 Subject: [PATCH 3/6] docs(triage): fold the audit's five blockers into the plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The audit caught a real design error: long context and Fast are mutually exclusive regimes, not composable multipliers — OpenAI's guide says long context is unsupported in Fast mode, so the $7.80 figure described an impossible request. Also records that -pro aliases would have been skipped by a base-id-only registry, that the main account has no WHAM single-flight to join, and that bounded claims need fairness ordering. --- .../260803_bug_backlog_stack/000_scope.md | 39 ++++++++---- .../020_long_context_pricing.md | 60 ++++++++++++++++--- .../030_cooldown_recovery.md | 52 +++++++++++++++- .../040_deepseek_residual.md | 14 +++++ .../050_jawcode_price_blocker.md | 23 +++++++ 5 files changed, 168 insertions(+), 20 deletions(-) diff --git a/devlog/_plan/260803_bug_backlog_stack/000_scope.md b/devlog/_plan/260803_bug_backlog_stack/000_scope.md index 6f9499935..b70047e09 100644 --- a/devlog/_plan/260803_bug_backlog_stack/000_scope.md +++ b/devlog/_plan/260803_bug_backlog_stack/000_scope.md @@ -105,14 +105,33 @@ rate) and MiniMax M3 at `> 512,000`. Dependency order. The stack is built bottom-up so each layer's diff is readable on its own. -| Phase | Doc | Unit | Depends on | +| Phase | Doc | Unit | Outcome | |---|---|---|---| -| 1 | `010` | Disposition sweep: labels + evidence comments | — | -| 2 | `020` | #908 long-context pricing tiers | — | -| 3 | `030` | #915 cooldown early-recovery probe | — | -| 4 | `040` | #545 and #875 — code or evidenced disposition | research | -| — | `050` | #907 blocker record (no code lands here) | — | - -#907 has no implementation phase on purpose. Its deliverable is the proof that -the fix belongs in `lidge-jun/jawcode`, plus the regression test that becomes -possible once the source moves. +| 1 | `010` | Disposition sweep: labels + evidence comments | applied | +| 2 | `020` | #908 long-context pricing tiers | code | +| 3 | `030` | #915 cooldown early-recovery probe | code | +| 4 | `060` | #545 classifier thinking round-trip | code | +| — | `040` | #875 residual — evidence, no code | comment | +| — | `050` | #907 price staleness — evidence, no code | comment | + +Two issues have no implementation phase on purpose, and each says why in its +own doc rather than being quietly dropped: #875's reopen evidence tested a +commit 115 before the fix, and #907's fix belongs in `lidge-jun/jawcode`. + +`#907` is not "unfixable". It is fixable — upstream. Under the current +source-of-truth policy it should not be fixed locally, which is a different and +more honest claim. + +## Stack shape + +#908, #915, and #545 touch disjoint files: `src/usage/`, `src/codex/`, and +`src/claude/` + `src/adapters/` respectively. Stacking them creates an +artificial ordering dependency where none exists in the code. + +They are stacked anyway, deliberately, because the user asked for a stack and +because the chain gives a reviewer one entry point and a stated review order +rather than three PRs landing on `dev` in arbitrary sequence. Each layer's +"Files changed" view still shows only that layer's diff, which is the property +the precedent was built for. If a maintainer prefers to take them +independently, any layer can be retargeted to `dev` without a rebase conflict — +that is worth saying in the stack-navigation comment. diff --git a/devlog/_plan/260803_bug_backlog_stack/020_long_context_pricing.md b/devlog/_plan/260803_bug_backlog_stack/020_long_context_pricing.md index 595f6406f..1d74af7ab 100644 --- a/devlog/_plan/260803_bug_backlog_stack/020_long_context_pricing.md +++ b/devlog/_plan/260803_bug_backlog_stack/020_long_context_pricing.md @@ -50,6 +50,52 @@ Provider scoping matters for the same reason: `cursor` and `openrouter` routes resell these models under their own terms and must not be charged first-party tier rules. +## Audit correction: long context and Fast are mutually exclusive + +The first draft of this phase composed the two multipliers — long context, +then Fast — and produced a $7.80 figure for a long Fast request. That request +cannot exist. OpenAI's Fast mode guide states plainly: "Long context, +fine-tuned models, and embeddings are not supported" +([fast-mode guide](https://developers.openai.com/api/docs/guides/fast-mode), +`verdict=strong_ok`, retrieved 2026-08-03). + +They are two regimes, not two factors. The correct model is an +either/or driven by the confirmed service tier: + +```text +base price → (Fast multiplier) OR (context multiplier) → calculateCost +``` + +A request marked `priority` never takes the context tier. The composition test +is replaced by an **exclusivity** test: a `priority` request above 272k gets the +Fast rate and `contextTier` stays undefined. + +### A second coupling this exposed + +`PRIORITY_MULTIPLIERS` (`src/usage/expected-prices.ts:152-156`) carries Terra +`1.6` and Luna `0.4`. Those are *ratios calibrated against the stale bases*. +Once #907 corrects Terra to 2/12, the 1.6 multiplier yields 3.2/19.2 while +OpenAI publishes Fast Terra at 4/24. Luna is worse: 0.4 × 0.20 = 0.08 against a +published 0.40. + +So #907 landing would silently break Fast estimates for two models. The +multipliers must move to verified absolute rates, or be recomputed against the +corrected bases, in the same change that corrects the bases. This is recorded +in `050` as part of the upstream follow-through — it is not this phase's work, +but it must not be discovered afterwards. + +## The `-pro` alias gap + +`gpt-5.6-sol-pro`, `-terra-pro`, `-luna-pro` are real selectable ids +(`src/providers/registry.ts:284-287`). The virtual-model resolver keeps the +*selected* id in `logCtx.model` and puts the wire id in `logCtx.resolvedModel` +(`src/providers/openai-virtual-models.ts:61-62`), and cost resolution +deliberately does not fall back through `resolvedModel`. + +So a tier registry keyed only on base ids would silently skip every `-pro` +request — and those are exactly the large ones. The registry carries explicit +provider-scoped rows for all three aliases, and each gets a test. + ## Design `src/usage/expected-prices.ts` — add beside `Cost4`: @@ -70,11 +116,8 @@ plus an exactly-keyed `CONTEXT_TIERS` registry (`${provider}\0${modelId}`) and `findContextTier()` / `isLongContext()`. No fuzzy matching, no case folding, no model-level fallback. Every row records its official URL and `verifiedAt`. -`src/usage/cost.ts` — add `applyContextTier()` and insert one stage: - -```text -base price → context multiplier → Fast multiplier → calculateCost -``` +`src/usage/cost.ts` — add `applyContextTier()`, applied only when the Fast +multiplier did not apply. `resolveMatchedPrice()` stays token-independent; it is memoized by provider/model (`src/usage/cost.ts:153-162`) and passing a token count would @@ -92,7 +135,8 @@ Sol, 300,000 input + 20,000 output, no cache: - short: `300000/1e6 × 5 + 20000/1e6 × 30` = `1.50 + 0.60` = **$2.10** - long: `300000/1e6 × 10 + 20000/1e6 × 45` = `3.00 + 0.90` = **$3.90** -- long + Fast: **$7.80** + +There is no long-Fast figure. That was the audit's first blocker. ## Tests @@ -107,8 +151,10 @@ resolution, attempts, combos, and Fast composition. 5. `cursor/gpt-5.6-sol` and `openrouter/openai/gpt-5.6-sol` stay untiered. 6. An untiered model is unchanged above every threshold. 7. The $2.10 / $3.90 worked example. -8. Fast composition → $7.80. +8. **Exclusivity**: a `priority` request above 272k takes the Fast rate and + leaves `contextTier` undefined. 9. Combo propagation: one long attempt + one standard attempt. +10. All three `-pro` aliases tier correctly at 272,001. The existing Fast fixture at `tests/usage-cost.test.ts:408` uses 1M input, which now crosses the threshold. It must move below 272k or its expected totals diff --git a/devlog/_plan/260803_bug_backlog_stack/030_cooldown_recovery.md b/devlog/_plan/260803_bug_backlog_stack/030_cooldown_recovery.md index 04aab99eb..16df1874f 100644 --- a/devlog/_plan/260803_bug_backlog_stack/030_cooldown_recovery.md +++ b/devlog/_plan/260803_bug_backlog_stack/030_cooldown_recovery.md @@ -79,9 +79,39 @@ a finite `weeklyPercent`, and recovery additionally requires not-exhausted. `src/codex/auth-api.ts` — add `runCodexCooldownRecoveryProbes()`, coalesced by a module-level promise, bounded by the existing `mapWithConcurrency(..., 4, -...)`, joining the existing per-account single-flight rather than opening a -parallel one. Registered on the state sweeper's existing 60s tick -(`src/lib/state-store-sweeper.ts:155-166`) — no new timer. +...)`. Registered on the state sweeper's existing 60s tick +(`src/lib/state-store-sweeper.ts:155-166`) — no new timer. Registration is +explicit at startup, after config and migrations complete +(`src/server/index.ts:290-333`); a module-load registration would not survive +the test resets that clear registrations while leaving modules cached. + +### Audit correction: the main account has no single-flight + +The first draft said the worker "joins the existing per-account single-flight". +That is true for pool accounts (`src/codex/auth-api.ts:646-660`, which matches +a flight by live generation) and **false for the main account**: +`fetchMainAccountInfo()` (`src/codex/auth-api.ts:366-369`) has no in-flight map +at all. The worker-level promise coalesces worker passes only, so a sweeper +probe of `__main__` could race a dashboard refresh or startup priming into two +parallel WHAM requests. + +The main account is therefore **excluded from the first cut**. Pool accounts +are where a multi-account pool starves a cooled account in the first place — +the reported scenario needs at least two accounts. Adding main-account recovery +requires giving main WHAM the same single-flight and admission semantics pool +has, which is its own change with its own race tests. + +### Audit correction: fairness + +Bounded claims with stable config-order enumeration can starve. Probe +eligibility recurs every five minutes (`src/codex/routing.ts:98`), so if more +accounts are due than `limit` and the early ones keep failing, they become due +again and consume the budget forever while later accounts are never reached. + +Claims are ordered by oldest `lastProbeAt ?? cooldownSince`, which rotates +naturally: a probed account moves to the back of the queue whether it recovered +or not. The test uses a pool larger than `limit` and asserts every cooled +account is probed within a bounded number of ticks. The worker never pauses an account, changes the active account, clears affinity, or synthesizes an upstream outcome. @@ -125,6 +155,22 @@ affinity (`:1186-1188`). Asserting otherwise would encode a policy change this phase is not making — the maintainer scoped it out in `devlog/_plan/260803_cooldown_recovery_probe/000_plan.md:41-43`. +## Line budget + +This phase spans routing, quota, auth-api, startup registration, and two test +files, and the case matrix above is large enough to approach the 500-line PR +guidance. Checkpoint before opening the PR: if the diff exceeds it, split the +claim/settle primitives in `routing.ts` (with their unit tests) from the worker +integration. The primitives are independently reviewable and the worker is +meaningless without them, so that split is dependency-correct. + +## A note on the clear→429 cycle + +Clearing a cooldown can lead to selection, a fresh 429, and another cooldown. +That is not a tight loop: the new 429 opens a new cooldown and the next probe +cannot occur for five minutes. It is the same cost as the account having been +eligible in the first place, which is the state we are trying to restore. + ## PR #922 overlap #922 touches neither `routing.ts`, `auth-api.ts`, nor `quota.ts`, so it does diff --git a/devlog/_plan/260803_bug_backlog_stack/040_deepseek_residual.md b/devlog/_plan/260803_bug_backlog_stack/040_deepseek_residual.md index 0269a9272..158da6757 100644 --- a/devlog/_plan/260803_bug_backlog_stack/040_deepseek_residual.md +++ b/devlog/_plan/260803_bug_backlog_stack/040_deepseek_residual.md @@ -94,3 +94,17 @@ item's `type`/`call_id`/`name`/`arguments`, and whether a second inbound `response.create` reached ocx at all. That last one splits the remaining space cleanly: if it arrives, the defect is downstream of the handoff; if not, it is the client's move that never came. + +## One test worth adding regardless + +`tests/ws-endpoint.test.ts:220-233` covers the JSON-to-WebSocket bridge with a +**message** item only. Nothing pins the function-call case — the exact shape +this issue turns on. + +A regression asserting that a `function_call` item in `output[]` is forwarded +as `response.output_item.done` with `call_id`, `name`, and `arguments` intact +would make the disposition above provable by test rather than by reading the +loop. It rides along with the #545 layer since neither touches the other's +files, and it is cheap insurance: the current behavior is correct by virtue of +an untyped `forEach`, which is exactly the kind of correctness that a future +refactor breaks silently. diff --git a/devlog/_plan/260803_bug_backlog_stack/050_jawcode_price_blocker.md b/devlog/_plan/260803_bug_backlog_stack/050_jawcode_price_blocker.md index 53056fc95..a9970022a 100644 --- a/devlog/_plan/260803_bug_backlog_stack/050_jawcode_price_blocker.md +++ b/devlog/_plan/260803_bug_backlog_stack/050_jawcode_price_blocker.md @@ -3,6 +3,14 @@ Outcome: **no code in this repository**. The fix belongs in `lidge-jun/jawcode`. +Precisely: this is not unfixable, and not blocked in the sense of "nothing can +be done". It is fixable upstream, and under the current source-of-truth policy +it should not be fixed locally. A new narrowly-scoped "verified corrections" +layer would be a technically legitimate design — `expected-prices.ts` is +precedent for exactly that shape — but it would deliberately diverge the +bundle from its canonical source to route around a one-line upstream edit. +Upstream-first is the cheaper and more honest order. + ## Why this is not a regeneration The obvious move is `bun run generate:jawcode-metadata` and commit the result. @@ -91,3 +99,18 @@ That test is the durable part. The existing sync test proves the bundle matches its source; it cannot prove the source matches the vendor. This one pins the actual published numbers, so the next price cut fails a test instead of silently overcharging users. + +## The coupled change nobody would have looked for + +`PRIORITY_MULTIPLIERS` (`src/usage/expected-prices.ts:152-156`) stores Fast +pricing as *ratios against the base rates* — Terra `1.6`, Luna `0.4`. Those +ratios were calibrated when the bases were stale. + +Correcting the bases therefore breaks Fast estimates for the same two models: +1.6 × 2 = 3.2 against a published Fast Terra of 4, and 0.4 × 0.20 = 0.08 +against a published 0.40. The regeneration that fixes #907 must recompute or +replace those multipliers in the same change, or it will fix an overcharge on +standard requests while introducing an undercharge on Fast ones. + +This is the kind of coupling a ratio-based design hides: the numbers stay +syntactically valid and no test fails. From 8c0350e3bb6ebaab6cc252d99bd50a000e239e4f Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 4 Aug 2026 00:25:09 +0900 Subject: [PATCH 4/6] docs(triage): fix the gate, the alias pricing, and the tier provenance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 2 of the audit found three more, all provable. Reusing usesAdaptiveThinking() as the disable gate would have emitted a body Fable rejects — and required breaking a passing test to do it. The -pro aliases are unpriceable today (runtime probe returns null), so a tier row alone could never be reached. And exclusivity must key on the response-confirmed tier, since a Fast request that exceeded 272k was necessarily downgraded. --- .../020_long_context_pricing.md | 44 ++++++++++++++-- .../060_classifier_thinking.md | 51 +++++++++++++++---- 2 files changed, 79 insertions(+), 16 deletions(-) diff --git a/devlog/_plan/260803_bug_backlog_stack/020_long_context_pricing.md b/devlog/_plan/260803_bug_backlog_stack/020_long_context_pricing.md index 1d74af7ab..d9272b227 100644 --- a/devlog/_plan/260803_bug_backlog_stack/020_long_context_pricing.md +++ b/devlog/_plan/260803_bug_backlog_stack/020_long_context_pricing.md @@ -66,9 +66,24 @@ either/or driven by the confirmed service tier: base price → (Fast multiplier) OR (context multiplier) → calculateCost ``` -A request marked `priority` never takes the context tier. The composition test -is replaced by an **exclusivity** test: a `priority` request above 272k gets the -Fast rate and `contextTier` stays undefined. +A request **confirmed** as Fast never takes the context tier. The composition +test is replaced by an **exclusivity** test. + +"Confirmed" is load-bearing. `effectiveServiceTier()` +(`src/usage/cost.ts:249-258`) collapses three sources into one scalar with the +precedence `responseServiceTier ?? requestedServiceTier ?? +configuredServiceTier`, and the estimator only ever sees the result. OpenAI +documents that a Fast request may be served as `default`, and that the +response's `service_tier` is what identifies the tier actually used — and Fast +does not support long context at all, so a >272k request tagged `priority` +was necessarily *not* served as Fast. + +Suppressing the context tier on a merely *requested* priority would therefore +under-bill exactly the request that provoked the downgrade. Exclusivity keys on +`responseServiceTier === "priority"`; a requested-or-configured priority with +no response confirmation takes the context tier. That needs tier provenance +preserved into the estimator rather than the collapsed scalar — a small +signature change at the three call sites in `shared.ts` and `summary.ts`. ### A second coupling this exposed @@ -93,8 +108,27 @@ but it must not be discovered afterwards. deliberately does not fall back through `resolvedModel`. So a tier registry keyed only on base ids would silently skip every `-pro` -request — and those are exactly the large ones. The registry carries explicit -provider-scoped rows for all three aliases, and each gets a test. +request — and those are exactly the large ones. + +But tier rows alone are not enough, and the audit proved why by running it: + +```console +$ bun run .tmp/probe_pro.ts +gpt-5.6-sol -> {"input":5,"output":30,"cacheRead":0.5,"cacheWrite":6.25} +gpt-5.6-sol-pro -> NULL (unpriceable) +gpt-5.6-terra-pro -> NULL (unpriceable) +gpt-5.6-luna-pro -> NULL (unpriceable) +``` + +The aliases have no base price at all today, and `estimateRequestCost()` +returns `null` the moment base-price resolution fails +(`src/usage/cost.ts:352`). Since the tier is applied *after* price resolution, +a `-pro` request could never reach `applyContextTier()` — the proposed alias +test would have failed against the real estimator. + +This phase therefore adds provider-scoped base-price rows for the three +aliases alongside their tier rows. That is a real bug of its own surfaced by +this work: `-pro` usage currently shows no cost estimate whatsoever. ## Design diff --git a/devlog/_plan/260803_bug_backlog_stack/060_classifier_thinking.md b/devlog/_plan/260803_bug_backlog_stack/060_classifier_thinking.md index e8c733c90..5f18c09c8 100644 --- a/devlog/_plan/260803_bug_backlog_stack/060_classifier_thinking.md +++ b/devlog/_plan/260803_bug_backlog_stack/060_classifier_thinking.md @@ -83,20 +83,48 @@ parser already understands — `"none"` is documented at ``` `src/adapters/anthropic.ts` — emit the explicit disable before the existing -non-`none` branch, gated on models where omission enables thinking: +non-`none` branch, gated on models that both default to thinking-on and accept +an explicit disable: ```diff -+if (parsed.options.reasoning === "none" && usesAdaptiveThinking(parsed.modelId)) { ++if (parsed.options.reasoning === "none" && supportsExplicitThinkingDisable(parsed.modelId)) { + body.thinking = { type: "disabled" }; +} else if (typeof parsed.options.reasoning === "string" && parsed.options.reasoning !== "none") { ``` -**Pre-write search (DEV-NECESSITY-01).** No new predicate is needed. -`usesAdaptiveThinking()` already exists at `src/adapters/anthropic.ts:411-421` -and version-gates precisely the families whose omission-default this depends on, -including date-pinned and suffixed ids. Adding a parallel model matcher would -duplicate a subtle regex that is already correct and already tested. +### Audit correction: `usesAdaptiveThinking()` is the wrong gate + +The first draft reused `usesAdaptiveThinking()` +(`src/adapters/anthropic.ts:411-421`) on the reasoning that it already +version-gates the right families. It does not — it answers a different +question, and the audit caught two ways it is wrong: + +- `ADAPTIVE_THINKING_FAMILY_MINIMUMS` includes `fable: [0, 0]` + (`src/adapters/anthropic.ts:405`), so **every** Fable model matches. Fable 5 + always has thinking enabled and rejects an explicit disable, so this gate + would emit a body that 400s. Worse, `tests/anthropic-reasoning.test.ts:139` + already asserts that `claude-fable-5` with `"none"` sends **no** thinking + config — the draft would have required breaking a passing test to ship a + production 400. That test was right and the plan was wrong. +- Opus 4.7/4.8 match the predicate but leave thinking off when the field is + omitted, so they need no disable at all. + +The predicate's own comment says what it is for: which families 400 on +`thinking.type: "enabled"` versus on `adaptive`. That is a *wire-shape* +question. "Does omission mean thinking is on, and is an explicit disable +accepted?" is a *capability* question, and the two sets genuinely differ. + +So this phase adds a separate `supportsExplicitThinkingDisable()` predicate, +seeded narrowly with the family this issue actually reproduces on — +`claude-sonnet-5` — and widened only per model with vendor evidence. Narrow is +also the right risk posture for an OAuth execution path. + +**Pre-write search (DEV-NECESSITY-01).** Reuse was checked first and rejected +on evidence rather than skipped: `usesAdaptiveThinking()` is the only existing +model predicate in this adapter, and it answers a different question. The new +predicate borrows its parsing shape so date-pinned and suffixed ids behave +identically. Explicitly unchanged: `max_tokens`, `stop_sequences`, the OAuth identity block, terminal mapping, retry behavior, native passthrough. @@ -108,10 +136,11 @@ terminal mapping, retry behavior, native passthrough. `undefined`) must **stay** `undefined`: that assertion is what proves disabled and omitted are no longer the same state, which is the entire fix. -`tests/anthropic-reasoning.test.ts` — adaptive model + `"none"` emits -`thinking: {type: "disabled"}`; adaptive model with reasoning omitted still -omits `thinking`; a non-adaptive model + `"none"` emits nothing; existing -enabled/adaptive paths unchanged. +`tests/anthropic-reasoning.test.ts` — Sonnet 5 + `"none"` emits +`thinking: {type: "disabled"}`; Sonnet 5 with reasoning omitted still omits +`thinking`; and the **existing** `claude-fable-5` + `"none"` assertion at +`:139-144` stays green unchanged, which is the regression proving the gate did +not widen past its evidence. Round-trip regression — the reporter's exact shape in, and assert the final adapter body carries `max_tokens: 64`, `stop_sequences: [""]`, From 151da16c7b7dcf4fe7045586875299e8918288c4 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 4 Aug 2026 00:27:40 +0900 Subject: [PATCH 5/6] docs(triage): make the exclusivity test match its own design, and count four call sites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test text still said 'any priority request' after the design moved to response-confirmed priority — implemented literally it would have reinstated the bug the amendment removed. And provenance has to reach four estimator call sites, not three: missing the per-provider one would make the dashboard total disagree with its own breakdown. --- .../020_long_context_pricing.md | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/devlog/_plan/260803_bug_backlog_stack/020_long_context_pricing.md b/devlog/_plan/260803_bug_backlog_stack/020_long_context_pricing.md index d9272b227..150d6115e 100644 --- a/devlog/_plan/260803_bug_backlog_stack/020_long_context_pricing.md +++ b/devlog/_plan/260803_bug_backlog_stack/020_long_context_pricing.md @@ -81,9 +81,23 @@ was necessarily *not* served as Fast. Suppressing the context tier on a merely *requested* priority would therefore under-bill exactly the request that provoked the downgrade. Exclusivity keys on `responseServiceTier === "priority"`; a requested-or-configured priority with -no response confirmation takes the context tier. That needs tier provenance -preserved into the estimator rather than the collapsed scalar — a small -signature change at the three call sites in `shared.ts` and `summary.ts`. +no response confirmation takes the context tier. + +That needs tier provenance preserved into the estimator rather than the +collapsed scalar, at **four** call sites — not three, which is what the first +amendment said: + +| Site | Surface | +|---|---| +| `src/server/management/shared.ts:129-130` | `/api/logs` per-entry cost | +| `src/usage/summary.ts:291-292` | `/api/usage` totals | +| `src/usage/summary.ts:420-421` | per-model breakdown | +| `src/usage/summary.ts:529-530` | per-provider breakdown | + +All four currently pass the collapsed `tier`. Updating three of them would +leave one cost surface still under-billing downgraded requests — and it would +be the kind of miss where the dashboard total disagrees with its own +per-provider breakdown for reasons nobody can reproduce. ### A second coupling this exposed @@ -185,8 +199,13 @@ resolution, attempts, combos, and Fast composition. 5. `cursor/gpt-5.6-sol` and `openrouter/openai/gpt-5.6-sol` stay untiered. 6. An untiered model is unchanged above every threshold. 7. The $2.10 / $3.90 worked example. -8. **Exclusivity**: a `priority` request above 272k takes the Fast rate and - leaves `contextTier` undefined. +8. **Exclusivity, by provenance**: an entry above 272k with + `responseServiceTier: "priority"` takes the Fast rate and leaves + `contextTier` undefined. An entry above 272k with priority only in + `requestedServiceTier` or `configuredServiceTier` — no response confirmation + — takes the **context tier**, because Fast does not serve long context, so + that request was downgraded. Both cases get a test; asserting only the first + would silently reinstate the bug this correction removes. 9. Combo propagation: one long attempt + one standard attempt. 10. All three `-pro` aliases tier correctly at 272,001. From 133d4ddf7c6c265050a057df5805884027b4368f Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 4 Aug 2026 03:13:45 +0900 Subject: [PATCH 6/6] docs(triage): record the stack outcome and what the audit gates caught MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five layers, sixteen bug issues dispositioned, zero closed on a partial fix. Twelve adversarial rounds, nine FAIL — including three #915 defects that were each introduced by fixing the previous one, which is the part worth remembering: a hand-written allowlist was never the right answer where the domain is an open string. --- .../260803_bug_backlog_stack/070_outcome.md | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 devlog/_plan/260803_bug_backlog_stack/070_outcome.md diff --git a/devlog/_plan/260803_bug_backlog_stack/070_outcome.md b/devlog/_plan/260803_bug_backlog_stack/070_outcome.md new file mode 100644 index 000000000..675ebac4e --- /dev/null +++ b/devlog/_plan/260803_bug_backlog_stack/070_outcome.md @@ -0,0 +1,106 @@ +# 070 — Outcome + +## What shipped + +A four-layer stack off `origin/dev` at `14b20def2`, each layer targeting the +one before it. + +| PR | Layer | Contents | +|---|---|---| +| #951 | 1/5 | triage: 22 label corrections + this plan unit | +| #952 | 2/5 | #908 long-context pricing tiers | +| #953 | 3/5 | six carried contributor bug fixes | +| #954 | 4/5 | #545 classifier thinking round-trip | +| #955 | 5/5 | #915 cooldown early-recovery probe | + +All four checks green on every layer. `enforce-target` passes on the stacked +children, which is the gate that decides whether this shape is allowed at all. + +## Disposition of the sixteen bug issues + +| Outcome | Issues | +|---|---| +| fixed in this stack | #908, #545, #915 | +| blocked upstream, evidenced | #907 (canonical source in `lidge-jun/jawcode`) | +| stale evidence, re-test requested | #875 (tested 115 commits before the fix) | +| reclassified | #919 → `enhancement` (deliberate policy, not a defect) | +| already owned by an open PR | #586, #893, #914, #938 | +| upstream trackers | #92, #241, #417 | +| awaiting reporter | #418, #796, #904 | + +Zero closed on a partial fix. Every open issue now carries at least two labels. + +## What the audit gates were worth + +Twelve adversarial rounds across three units, nine FAIL. + +On the plan (4 rounds, 3 FAIL): long context × Fast composition is impossible — +OpenAI does not serve long context in Fast mode, so the `$7.80` figure +described a request that cannot exist. The `-pro` aliases were unpriceable, so +a tier row could never have been reached and the proposed test would have +failed against the real estimator. `usesAdaptiveThinking()` was the wrong gate +and would have required breaking a passing test to ship a 400. + +On the #545 code (3 rounds, 2 FAIL): the gate silently missed +`anthropic/claude-sonnet-5`. Then **my own remediation was worse than the bug** — +normalizing with `lastIndexOf("/")` fixed the prefix case and broke the suffix +case, and since the adaptive-wire predicate shares that parse, a slash-suffixed +Sonnet 5 would have been sent obsolete manual `thinking.enabled` and 400d. A +silent truncation traded for a hard failure, caught only because the reviewer +re-ran the comparison across the full existing matrix rather than trusting the +four ids I had listed. + +Two of the plan blockers would have shipped code that passed CI and was wrong. + +On the #915 code (5 rounds, 4 FAIL) the failures compounded, and each one was a +smaller version of the same mistake: + +1. Unknown plans failed **open** — an unfamiliar `plan_type` cleared a cooldown + on evidence we could not interpret. +2. The fix for that was an allowlist, which missed `prolite`. +3. The allowlist was then the wrong *shape*: the snapshot carries 21 plan + strings with 12 unclassified, and `CodexAccount.plan` is unrestricted. Every + omission meant an account cooled **forever** — this unit's own defect, + reintroduced as a typo-shaped hole. Replaced with the parser's binary rule, + now shared by parsing, exhaustion, and recovery. +4. The replacement test computed its expectation from the function under test, + so ablating the rule to always-weekly still passed 18/18. + +Three of those four were introduced by *fixing the previous one*. The lesson is +not that lists need care; it is that a list was never the right answer where the +domain is an open string. + +Three tests also passed vacuously and were rebuilt: both lease-release cases +asserted only that the cooldown survived, never that the lease returned (a +stranded lease means that account is never probed again), and the fairness test +spaced its passes inside the probe interval, so already-probed accounts dropped +out on their own and the ordering was never exercised. + +## Two hypotheses that did not survive + +**#545's recorded cause was impossible.** The analysis said the prepended OAuth +identity block consumed the classifier's 64 output tokens. The identity goes +into the system prompt; `max_tokens` caps output. This repository had already +rejected that theory once — `devlog/_fin/260728_bug_bundle_resolution/030_claude_system_dedup.md` +abandoned an identity-dedup patch for the same reason — and it was re-derived +anyway. The real cause was a round-trip loss where `disabled` and `omitted` +collapsed to one representation, and omitted means thinking-**on**. + +**#875's reopen was correct on the evidence and wrong on the facts.** The +reporter tested `0b30283b6`; `git merge-base` puts it 115 commits before +`5dd965a13`, the commit that fixes exactly that path. Left open with a re-test +request rather than closed on an argument the reporter cannot check from their +side. + +## Carried contributor work + +Six PRs cherry-picked with `-x`, authorship intact, no content changes: +#939, #943, #944, #945 (@DevMello), #942 (@L14nY1Wang), #948 (@mushikingh). + +Not carried, each for a stated reason: #947 conflicts with #942 on the same +relay path and resolving that is its author's call; #933 is a draft with a +failing gate; #928, #922, #940, #935 carry `CHANGES_REQUESTED`, and carrying a +PR past requested changes routes around the review. + +The source PRs stay open until #953 lands, so the direct path remains available +if a maintainer prefers it.