diff --git a/devlog/_plan/260819_next_roadmap/000_roadmap.md b/devlog/_plan/260819_next_roadmap/000_roadmap.md new file mode 100644 index 0000000000..cc043140cd --- /dev/null +++ b/devlog/_plan/260819_next_roadmap/000_roadmap.md @@ -0,0 +1,153 @@ +# 260819 — Next roadmap (post-triage, post-cleanup) + +> **Status: executed 2026-08-19.** R1-R4 are closed; see `050_execution_ledger.md` +> for what happened and `060_outcome.md` for the closeout. Two sections of this +> document were corrected by audit lanes during execution — the corrections are +> in `010` and `030`, and the reasons are in `050`. Read this file for the +> sequencing decision, not for current PR state. + +Baseline: dev @ 3ad904e03 (local, 1 ahead of origin/dev 63bfd149d). +Inputs: the 260819 triage-execution outcome (12 merged, 5 downgraded), the +260818 mega-file split risk assessment, and a live read of 53 open PRs / +75 open issues on 2026-08-19. + +This document is a sequencing decision, not an inventory. The inventory is in +`260819_triage_execution/030_outcome.md`; what follows is what to do next and +in what order, with the constraint that makes the order non-arbitrary. + +## The one constraint that orders everything + +Five files carry the repository: `responses/core.ts` (4532), `config.ts` +(3987), `service.ts` (3387), `registry.ts` (2692), `types.ts` (1884). +The split program wants to move all of them. Twenty-plus open PRs edit them. +A split lands as a whole-file rewrite, so **every open PR touching a split +file is rebased onto moved code the moment the split merges.** + +The 000_risk_assessment already named this ("never interleave"). What has +changed since it was written is that the interleaving already happened: the +WP1 stack sat while 102 commits landed on dev, and it now conflicts. + +So the order is: **drain the contributor queue first, split second.** Not +because the split is less valuable, but because the split's cost is +proportional to the size of the queue it has to rebase, and the queue is the +thing that decays if left alone. + +## R1 — Unblock the split stack (this week, small) + +The WP1 types stack is the cheapest split and it is currently the most +expensive to leave alone: it conflicts with dev today and the conflict grows +with every `types.ts` edit. + +| PR | State | Action | +|---|---|---| +| #2019 WP1 | draft, 102 behind, red CI | rebase onto dev, re-run; the red is stale-base, not the change | +| #2023 WP1b | draft, child of #2019 | rebase after parent; green on its own base already | +| #2036 WP2a | draft, 42 behind | rebase; independent of WP1 | + +**The red CI on #2019 is not a defect in the change.** Its failing shard +asserts `invalidateCodexModelsCacheWithPermit(permit, owningCodexHome)`, +a string that exists on the PR head and no longer exists on dev — dev removed +it in `6c0bde453`. The PR is running dev's newer test file against its own +older source. Three other shards fail the same way (hidden raw reasoning, +Command Code catalog, GUI models page). A rebase is the whole fix. + +Both stacks also fail `hygiene: missing_regression_test`, which is correct +and not waivable by rebase: a pure-move PR changes `src/` without changing a +test. The honest resolution is `test-exception-approved`, not a decorative +test — the barrel's oracle is the existing 400-file import surface, and a new +test asserting "the barrel re-exports X" restates the compiler. + +## R2 — Land the response-temp stack (ready now) + +`#2084` (sweeper) and `#2089` (doctor) are the only PRs of ours that are +green, hygiene-clean, and 5 commits behind. `#2089` is `CLEAN`; `#2084` is +`BLOCKED` only by the review requirement. + +Merge order is forced: `#2084` to dev, then retarget `#2089` from +`codex/tmp-reclaim-1-sweeper` to dev. Do not merge the child first. + +This closes a real user-visible defect (multi-GB temp accumulation across +reboots) and the design holds up on read: the boot floor retires a vacuous +PID probe rather than claiming a file is dead, `eligible` is reported instead +of `matched` so the operator is not told live temps are abandoned, and the +dry run shares one predicate with the reclaim so report and removal cannot +disagree. + +## R3 — Resolve the duplicate-fix collisions (before more arrive) + +Three PRs fix the same `prompt_cache_retention` bug (#2092) three different +ways, and they are mutually incompatible: + +| PR | Scope | Consequence | +|---|---|---| +| #2091 | strip for all ChatGPT-backend Responses | broadest; also drops it where a deployment honors it | +| #2099 | strip for `gpt-5.6*` prefix, forward mode | wrong branch (targets main) | +| #2102 | strip for `gpt-5.6`/`gpt-5.6-`, passthrough | narrowest and most precise | + +Pick one and close the other two with the reason. On the evidence in the +issue, #2102's model-scoped strip is the defensible default: the backend's +cache behavior varies by deployment, so a global strip silently removes a +parameter some accounts accept. + +A second collision: `#2056` and `#2062` both address K12 short-window quota, +and `#2062` targets main. + +**Eight PRs currently target `main` and are auto-labeled `[WRONG BRANCH]`:** +#2110, #2109, #2099, #2082, #2063, #2062, #2032, #2029. These cannot merge +as-is. Retarget or close — leaving them open costs contributor goodwill and +re-triage time on every pass. Note that #2099 and #2062 appear in the +collision lists above, so retargeting them and picking a winner is one +decision, not two. + +## R4 — The `modelRecordValue` family (one review, four PRs) + +`#2077`, `#2085`, `#2086`, `#2100` are the same fix applied at four call +sites: a bare `map[modelId]` lookup where the runtime uses `modelRecordValue`, +so a `gpt-oss` entry fails to cover `gpt-oss:120b`. They are independent, +small, and each carries a focused test. + +Review them as one batch with one shared verdict on the contract, then merge +individually. Reviewing them separately spends four reviews on one idea. + +`#2077` additionally fixes a real crash path: a routed model id of +`constructor` or `toString` returned an `Object.prototype` function through +the prototype chain, which made `buildBehaviorFingerprintV1` throw inside a +linker that contractually does not throw. + +## R5 — Then, and only then, the split program + +Resume at WP2b (the stateful config train) with the sequencing rule from +000_risk_assessment intact: one work package per PR train, service and +registry never in the same change, Wave C never mixed with behavior fixes. + +The rule that matters most is the oracle rule: a guard test rewritten in the +same PR as the code it guards must be driven red once against a deliberate +violation. `core-lab-boundary` and `repo-hygiene` already follow it; WP5 +Wave C rewrites seven source-invariant tests and cannot be exempt. + +## What this roadmap deliberately does not do + +- **No new feature work is scheduled.** The queue has 53 open PRs; adding + scope before draining it makes the split more expensive, not less. +- **`#1704` (combo quota badges) stays parked.** It is 817 commits behind and + `CONFLICTING`. It is a re-cut, not a rebase, and it should be re-cut against + the GUI as it exists after the split — not before. +- **The Antigravity stack (#2068-#2071) is not sequenced here.** Four PRs, + ~5600 added lines, one author, all `BLOCKED`. It needs its own review lane + and its own decision about landing order; folding it into a general roadmap + would understate that. + +## Branch hygiene (done 2026-08-19) + +Local 106 -> 25, origin 66 -> 22. Every deleted branch was verified merged +into `origin/dev` or backed by a `MERGED`/`CLOSED` PR, with SHAs recorded in +`.tmp/branch-cleanup-*.txt` so any deletion is recoverable. + +Release branches (`release-2.25.0`, `release-2.26.0` and their previews, +`codex/promote-*`) were deleted only after confirming each is an ancestor of +`main` or `preview` and preserved by its `v*` tag. + +Six branches showed as "unmerged" while their PRs read `MERGED` — squash +merges, where the branch commit never enters dev's ancestry. Each was +confirmed by locating its merge commit in dev before deletion. A plain +`--merged` filter would have missed all six and left them to rot. diff --git a/devlog/_plan/260819_next_roadmap/010_r1_split_rebase.md b/devlog/_plan/260819_next_roadmap/010_r1_split_rebase.md new file mode 100644 index 0000000000..569f4d262d --- /dev/null +++ b/devlog/_plan/260819_next_roadmap/010_r1_split_rebase.md @@ -0,0 +1,98 @@ +# 010 — R1: rebase the mega-file split stack + +Work-phase: wp2. Scope: **review + rebase + push. No merges.** + +## Why the CI is red, precisely + +**Corrected 2026-08-19 after an independent audit lane refuted the first +version of this section.** The original text named the right mechanism and the +wrong direction; both corrections are below. + +The failing assertion in shard 2/4 is a source-invariant test that reads code +as text. Run `32130164359` reports: + +``` +Expected to contain: "invalidateCodexModelsCacheWithPermit(permit, owningCodexHome)" +Received source: invalidateCodexModelsCacheWithPermit(permit, owningCodexHome, { allowWhenDesiredDisabled: true }) +``` + +So the **test** carried the old two-argument assertion and the **source** +carried the new three-argument call. The three-argument call arrived on dev in +`91979cf14`; `6c0bde453` then updated the assertion to match — but it landed +*after* this run. GitHub merges the PR head with the base before running CI, so +the run executed **dev's newer source against the PR's older test**. + +That is the inverse of what this document first claimed ("dev's newer test +against the PR's older source"). The conclusion — stale-base merge skew, not a +defect in the extraction — survives; the mechanism statement had to be fixed. + +The failure set is also larger than first recorded. Actual reds: test 1/4 +(hidden raw reasoning), 2/4 (the sync-cache assertion above), 3/4 (Command Code +catalog), 4/4 (server local API auth), gates (Models-page GUI), and macos +(multiple). Six legs, not four. + +The control: `#2023` is a strict history superset of `#2019` +(`git merge-base --is-ancestor 194f9f2a b2ac2500` exits 0), and **every +cross-platform job passes on its base** — shards 1-4, gates, macos, keyring, +npm-global. It is *not* "fully green": `hygiene` and `enforce-target` fail on +it, as they do on every PR in this stack. + +**What the control does and does not prove.** It shows the extraction does not +break the suite when the suite and the source agree. It does not prove the +rebased `#2019` head is defect-free against *current* dev — only a CI run on +the new head can. Treat "stale base" as the hypothesis this rebase tests, not +as an established fact. + +## Verified rebase cost + +A scratch rebase of `codex/split-wp1b-type-clusters` onto `origin/dev` +(102 commits) conflicts in exactly one file, `src/types.ts`, with **3 hunks**. + +Three dev commits touched `src/types.ts` since the fork point (`b04cd26e7`): + +| Commit | Change | +|---|---| +| `11e03eb44` | replay: durable thought signatures per credential (#2078) | +| `fd85c8238` | cursor: HTTP/1.1 compatibility transport (#1903) | +| `b5a98d690` | release-audit regressions from the 260818 merge train | + +All three add or modify type declarations. Because WP1b turns `types.ts` into +a pure barrel, each conflict resolves the same way: **the new declaration moves +to the leaf that owns its cluster, and the barrel gains a re-export line.** +This is mechanical, but it is not automatic — resolving it by taking "ours" +would silently drop three landed changes. + +## Order + +`#2019` and `#2036` are independent; `#2023` is a child of `#2019`. + +1. Rebase `codex/split-wp1-types` onto `origin/dev`; resolve `types.ts`; + force-push. Confirm the four shards go green. +2. Rebase `codex/split-wp1b-type-clusters` onto the NEW `#2019` head, not onto + dev. Rebasing it onto dev directly would orphan the parent PR's diff. +3. Rebase `codex/split-wp2a-config-names` onto `origin/dev` (42 behind, already + green — this is upkeep, not a fix). + +All three branches are ours (`lidge-jun`), so force-push is in scope. + +## The hygiene failure is real and is not fixed by rebasing + +All three PRs fail `hygiene: missing_regression_test`: they change `src/` +without changing a test. That gate is correct here — and the honest answer is +`test-exception-approved`, not a manufactured test. + +A pure-move PR's oracle is the ~400 test files that import through the barrel +plus `tsc --noEmit`. A new test asserting "the barrel re-exports `OcxTool`" +restates what the compiler already proves and would pass even if the extraction +were wrong in every way that matters. + +Verified: dev's `src/types.ts` exports 85 names; the WP1b barrel re-exports all +85 across six leaves (`tools`, `wire`, `request`, `config`, `provider`, +`accounts`), reducing 1884 lines to 103. + +## Exit criteria + +- `c-2019`: new head pushed; the four previously-red shards no longer FAILURE. +- `c-2023`: rebased onto the new parent head; base ancestry correct. +- `c-2036`: rebased onto dev; still green. +- No merges. No `src/` change beyond conflict resolution. diff --git a/devlog/_plan/260819_next_roadmap/020_r2_temp_reclaim_merge.md b/devlog/_plan/260819_next_roadmap/020_r2_temp_reclaim_merge.md new file mode 100644 index 0000000000..93e7af0ac2 --- /dev/null +++ b/devlog/_plan/260819_next_roadmap/020_r2_temp_reclaim_merge.md @@ -0,0 +1,79 @@ +# 020 — R2: merge the response-state temp reclaim stack + +Work-phase: wp1. Scope: **the only merges authorized this session.** + +## State at plan time + +| PR | Head | Base | Checks | mergeStateStatus | +|---|---|---|---|---| +| #2084 sweeper | `816024c95` | `dev` | zero FAILURE | `BLOCKED` (review requirement only) | +| #2089 doctor | `3cb6bb497` | `codex/tmp-reclaim-1-sweeper` | zero FAILURE | `CLEAN` | + +Both are hygiene-green (they carry real tests) and 5 commits behind dev. +`#2084`'s `BLOCKED` is the review-requirement ruleset, which admin merge +passes — it is not a failing check. + +## Order is forced + +`#2089` targets `#2084`'s branch. Merging the child first would land the +doctor command on top of a sweeper that is not on `dev`. + +1. Merge `#2084` into `dev`. +2. Retarget `#2089` base from `codex/tmp-reclaim-1-sweeper` to `dev`. + GitHub rewrites the child's diff on retarget; confirm it shows only the + doctor changes afterward, not the sweeper's. +3. Merge `#2089`. + +After step 1 the parent branch is deletable, but **not before step 2** — deleting +the base of an open PR closes it. + +## What the change actually does (read, not summarized from the title) + +The defect: `~/.opencodex` accumulates multi-GB of +`responses-state.json.ocx...tmp`, growing after every reboot. + +Root cause is two-part, and the second part is the interesting one: + +1. The existing cleanup ran **once per process, at cache load** — before that + process writes anything. So a crashed-and-restarted proxy swept too early to + see its predecessor's temp (15-minute grace) and never looked again. +2. The cleanup **skipped any file whose owning PID was still alive**. After a + reboot the OS reissues PIDs, so an old file is permanently mistaken for a + live process's. That is why growth tracked reboots. + +### Design points that hold up on review + +- **The boot floor retires a vacuous probe, it does not claim death.** A temp + older than the current boot cannot be owned by the PID we would probe, so the + probe is meaningless and is skipped. The comment is explicit that this does + not prove the file is dead; the unconditional 15-minute grace remains the + safety floor. +- **An anomalous boot time disables the floor rather than clamping it.** + Clamping a future-dated boot to "now" would retire the liveness probe for + every file past the grace — the worst possible response. Absent floor costs a + missed reclaim; a wrong floor costs a live file. +- **`ENOENT` on unlink counts as reclaimed, not failed.** Another proxy sharing + the config dir may have won the race; reporting that as failure would tell an + operator a file is "in use or locked" when nobody holds it. +- **The sweep covers the resolved directory too.** Atomic writes place the temp + beside the *resolved* target, so a symlinked config dir strands temps where a + literal-dir scan never looks. +- **The periodic pass rides the liveness tick, not the TTL tick**, because + `sweepExpiredOnWrite` puts `sweepExpired` on hot write paths and a directory + scan does not belong there. It carries a 25 ms wall-clock deadline: an entry + cap bounds syscalls, not time, and on a network-mounted config dir each + `lstat` can cost 10-20 ms. +- **The doctor reports `eligible`, never `matched`.** `matched` increments + before the file-type, age, boot-floor and liveness gates, so reporting it + would tell an operator that live-PID and young temps are abandoned. +- **Report is the default; reclaim is opt-in** behind + `--reclaim-response-temps`, and a typo'd `--reclaim*` flag warns instead of + silently degrading to "nothing to reclaim". +- **Dry run and reclaim share one predicate**, so the report and the subsequent + removal cannot disagree about which files are reclaimable. + +## Exit criteria + +- `c-2084`: `gh pr view` state MERGED; merge commit is an ancestor of + `origin/dev`; post-merge CI inspected on the merge SHA. +- `c-2089`: base reads `dev`; state MERGED; ancestry proof; CI inspected. diff --git a/devlog/_plan/260819_next_roadmap/030_r3_collisions_and_retargets.md b/devlog/_plan/260819_next_roadmap/030_r3_collisions_and_retargets.md new file mode 100644 index 0000000000..83b72db307 --- /dev/null +++ b/devlog/_plan/260819_next_roadmap/030_r3_collisions_and_retargets.md @@ -0,0 +1,131 @@ +# 030 — R3: duplicate-fix collisions and wrong-branch retargets + +Work-phase: wp3. Scope: **review, retarget, rebase. No merges.** + +## Collision A — `prompt_cache_retention` (issue #2092), three PRs + +The ChatGPT codex backend 400s on gpt-5.6 models when +`prompt_cache_retention` is forwarded. Three PRs fix it three incompatible +ways. Only one can land. + +| PR | Where it strips | Predicate | Base | +|---|---|---|---| +| #2091 luvs01 | `stripUnsupportedForwardParams` | ALL ChatGPT-backend Responses, any model | dev | +| #2099 yzxcj797 | new `stripPromptCacheRetentionForGpt56`, forward path | `modelId.startsWith("gpt-5.6")` | **main** | +| #2102 lilinxiong | new `stripDeprecatedPromptCacheRetention`, passthrough | `=== "gpt-5.6" || startsWith("gpt-5.6-")` | dev | + +**Recommendation: #2102.** + +The deciding evidence is in #2099's own comment: one deployment accepted +`"24h"` and echoed it back. The backend's cache handling is account-level and +has provably varied. So a global strip (#2091) silently removes a parameter +that some accounts honor — it fixes the report by making the feature +unavailable to everyone. + +Between the two model-scoped fixes, #2102 is on the right branch and its +predicate is tighter: `startsWith("gpt-5.6")` (#2099) also matches a +hypothetical `gpt-5.60`, while `"gpt-5.6"` exact-or-`"gpt-5.6-"`-prefixed +cannot. #2102 also tests four concrete ids rather than one. + +#2102 declines to translate `24h` into the replacement +`prompt_cache_options.ttl`, and says so: GPT-5.6 uses a different TTL +contract, and implicit caching still applies when the caller sent no +replacement. Inventing a translation would be the one change here that could +alter billing behavior. + +Action: recommend #2102; close #2091 and #2099 with this rationale. #2099 is +also on the wrong branch and appears in the retarget list below — retargeting +it and closing it are the same decision, taken once. + +**One risk to raise with #2102 rather than silently accept.** Its sanitizer is +called *outside* the `if (forward)` branch, so it also strips the field from +API-key and third-party `openai-responses` passthroughs — not just the ChatGPT +forward path the issue is about. Current OpenAI guidance (replace +`prompt_cache_retention` with `prompt_cache_options.ttl` on GPT-5.6) makes +that defensible for genuine OpenAI endpoints, but the tests exercise only the +forward-mode provider. Ask for an API-key regression test and an explicit +decision about custom OpenAI-compatible endpoints before merging. + +## Collision B — K12 short-window quota (issue #2047), two PRs + +This one is **not** a duplicate, and reading it as one would reintroduce a bug +we already caught. + +`#2056` (Ingwannu, base dev) was reviewed and held as needs-work for a +specific reason: a fail-open in `computeCodexUsageScore`. An account whose +cached quota carries only `shortPercent` — no weekly, no monthly — scored 0 +instead of `CODEX_UNKNOWN_USAGE_SCORE` (101), making an account with +*unverified* long-window quota look like the coolest candidate to +`pickLowestUsage*`. Short-only WHAM snapshots do enter the valid cache, so this +is reachable. + +`#2062` (yzxcj797, base main) fixes the same issue and **has the same +fail-open**. Its `computeCodexUsageScore`: + +- 30-day plans: when `monthlyPercent` is absent, `return burst !== undefined ? burst : CODEX_UNKNOWN_USAGE_SCORE` — a short-only account scores `burst`, so `shortPercent: 0` scores 0. +- other plans: `burst` joins `values`, so a short-only account scores `max([burst])` = `burst`. + +Its test suite covers the saturated-burst direction +(`{weeklyPercent: 1, shortPercent: 100}` → 100) but never the short-only case +the review flagged. + +`pickLowestUsageAmong` keeps the lowest score, so a short-only 0 beats an +account with verified long-window usage. + +**Reachability differs from #2056, and an audit lane was right to narrow +this.** #2056 adds `shortPercent` to `hasKnownQuotaValue`, so a short-only +WHAM snapshot enters the valid cache and reaches the scorer on the ordinary +parser path. #2062 does not: its `hasKnownQuotaValue` still checks only weekly +and monthly, so a short-only parse returns `null`. On #2062 the fail-open is +reachable through unvalidated disk hydration or direct cache insertion, not +through a normal WHAM response. + +That makes #2062's fail-open narrower, not absent — and it is still the same +defect class the #2056 review named, sitting in code that will be asked to +accept short-only state as soon as anyone finishes the feature. + +**A second defect in #2062 that #2056 does not have:** it preserves the short +tuple only during `creditsOnly` refreshes. A later weekly/monthly partial +snapshot without short fields rebuilds `next` and drops the tuple, and +`updateAccountQuota` likewise omits existing `shortPercent`/`shortResetAt`/ +`shortWindowSeconds`. #2056 handles both preservation cases explicitly. + +So neither supersedes the other: on preservation #2056 is ahead, on +reachability #2062 is accidentally safer, and both carry the scoring +fail-open. Neither should merge until the short window is treated as an +**additional pressure signal gated on a governing long window being present**. + +Action: neither merges this phase. Post the shared root cause on both, so two +contributors are not each debugging half of it, and name the preservation gap +on #2062 specifically. + +## Wrong-branch retargets — eight PRs + +All eight target `main`, are auto-titled `[WRONG BRANCH]`, and are draft. +`main` only moves by maintainer promotion, so none can merge as-is. + +| PR | Author | Head | Mergeable | +|---|---|---|---| +| #2110 | drakonkat | `fix/antigravity-allow-baseurl-override` | MERGEABLE | +| #2109 | drakonkat | `fix/anthropic-allow-baseurl-override` | MERGEABLE | +| #2099 | yzxcj797 | `fix/pcr-strip-gpt56-2092` | MERGEABLE | +| #2082 | yzxcj797 | `fix/agr-language-preamble-2074` | MERGEABLE | +| #2063 | yzxcj797 | `fix/k12-detail-denial-2046` | **CONFLICTING** | +| #2062 | yzxcj797 | `fix/k12-short-window-quota-2047` | MERGEABLE | +| #2032 | yzxcj797 | `fix/claude-root-bypass-sandbox-1688` | MERGEABLE | +| #2029 | yzxcj797 | `fix/no-session-bus-absent-1939` | MERGEABLE | + +**Every head lives in a contributor fork.** We retarget the base with `gh`; we +do not touch their branches. Rebasing a fork head is the author's job, and +force-pushing someone else's branch is out of scope by the objective. + +`#2063` is CONFLICTING and also overlaps `#2055`, which already merged as a +partial fix for #2046. It needs a diff against current dev before it is worth +the author's rebase. + +## Exit criteria + +- `c-pcr`: winner chosen with written rationale traced to actual diffs. +- `c-k12`: decision recorded, with the shared fail-open named on both PRs. +- `c-wrongbranch`: all eight read `baseRefName=dev`. +- Zero merges in this work-phase. diff --git a/devlog/_plan/260819_next_roadmap/031_r3_posted_decisions.md b/devlog/_plan/260819_next_roadmap/031_r3_posted_decisions.md new file mode 100644 index 0000000000..90526f4687 --- /dev/null +++ b/devlog/_plan/260819_next_roadmap/031_r3_posted_decisions.md @@ -0,0 +1,50 @@ +# 031 — R3 decisions as posted + +The decision record is 030. This file records what was actually said on each +PR and where, so a later reader can check the public artifact against the +private reasoning without re-reading eight threads. + +## `prompt_cache_retention` (#2092) + +| PR | Action | Comment | +|---|---|---| +| #2102 | **selected** | [5340666298](https://github.com/lidge-jun/opencodex/pull/2102#issuecomment-5340666298) | +| #2091 | not selected | [5340672074](https://github.com/lidge-jun/opencodex/pull/2091#issuecomment-5340672074) | +| #2099 | not selected + retargeted | [5340672361](https://github.com/lidge-jun/opencodex/pull/2099#issuecomment-5340672361) | + +Both non-selected authors were told *why*, not just that they lost. #2091's +instinct (fix it at the strip site) and #2099's instinct (keep it model-scoped) +were each named as correct — #2099's is the reason #2091 was not chosen either. + +The one open request on #2102: its sanitizer is called outside the +`if (forward)` branch, so it also strips from API-key and third-party +`openai-responses` passthroughs. Defensible for real OpenAI endpoints, untested +for custom ones. + +## K12 short-window quota (#2047) + +| PR | Action | Comment | +|---|---|---| +| #2056 | hold | [5340681696](https://github.com/lidge-jun/opencodex/pull/2056#issuecomment-5340681696) | +| #2062 | hold + retargeted | [5340672627](https://github.com/lidge-jun/opencodex/pull/2062#issuecomment-5340672627) | + +The same root cause was posted on both so two contributors are not each +debugging half of it, and the asymmetry was stated in each direction rather +than framed as one PR being better: #2062 is narrower on reachability, #2056 is +ahead on preservation, and both carry the scoring fail-open. The suggested +combination — #2056's preservation handling plus a gated scorer — is on the +thread. + +## Retargets + +Eight PRs moved `main` -> `dev`: #2110, #2109, #2099, #2082, #2063, #2062, +#2032, #2029. Verified after with `gh pr view`: all eight report +`baseRefName=dev`; seven are `MERGEABLE`. + +`#2063` is `CONFLICTING` and overlaps the already-merged #2055, so it got a +separate note ([5340672862](https://github.com/lidge-jun/opencodex/pull/2063#issuecomment-5340672862)) +asking for a rebase and a rescope rather than a silent retarget. + +**No contributor head was rewritten.** Every one of those heads lives in a fork +(`drakonkat`, `yzxcj797`). Retargeting a base is maintainer work; rebasing +someone's branch is theirs. diff --git a/devlog/_plan/260819_next_roadmap/040_r4_modelrecordvalue_batch.md b/devlog/_plan/260819_next_roadmap/040_r4_modelrecordvalue_batch.md new file mode 100644 index 0000000000..1146766293 --- /dev/null +++ b/devlog/_plan/260819_next_roadmap/040_r4_modelrecordvalue_batch.md @@ -0,0 +1,138 @@ +# 040 — R4: the `modelRecordValue` family, reviewed as one batch + +Work-phase: wp4. Scope: **review only. No merges.** + +## Why one batch and not four reviews + +`#2077`, `#2085`, `#2086`, `#2100` are the same one-line idea applied at four +call sites. Reviewing them separately spends four independent judgments on a +contract that only has to be decided once. + +## The shared contract + +**Corrected 2026-08-19 after a review lane refuted the first version.** The +contract as originally written implied every per-model map should migrate to +`modelRecordValue`. That is false, and acting on it would have shipped two +regressions. + +`modelRecordValue` (`src/reasoning-effort.ts:73`) resolves in this order: + +1. exact model id, **own property only**; +2. if the id contains `:` past the first character, the **case-sensitive** + pre-colon family, own property only; +3. a case-insensitive match on the **full** id across own entries; +4. otherwise `undefined`. + +Two subtleties the first draft missed: there is no case-insensitive *family* +match, and a case-sensitive family key beats a differently-cased full-id key. + +The contract, restated correctly: **code that reports, gates, or describes what +the runtime will do with a per-model override must use the same resolution the +runtime uses for that map** — which is not `modelRecordValue` for every map. + +Two maps are deliberately exact-own-only: + +| Map | Runtime reader | +|---|---| +| `modelPreferHostedTools` | `src/adapters/openai-responses.ts:989` (exact, own-property) | +| `modelOpenRouterRouting` | `src/providers/openrouter-routing.ts:83` (exact, own-property) | + +For those, a bare `map?.[modelId]` is still wrong — it walks the prototype +chain — but `modelRecordValue` is *also* wrong, because it adds family and +case-folded inheritance the adapter will not honor. The right primitive there +is an exact own-property lookup, not either of the two. + +So the family-aware migration is correct for nine maps and a regression for +two. "Read it the way the runtime reads it" is the invariant; "use +`modelRecordValue`" is only its implementation for the family-aware set. + +## Why the failure mode is worse than "missing an entry" + +In `#2085` and `#2100` the bare lookup does not degrade to unknown; it **falls +through to the provider-wide value**. That is a definite wrong answer rather +than an absent one. `#2085`'s case: `modelContextWindows: {"gpt-oss": 131072}` +with a request for `gpt-oss:120b` resolved nothing, fell back to +`contextWindow: 8000`, and the admission gate refused turns the model can +plainly hold. + +`#2077` carries a second, sharper defect worth calling out separately: the bare +index **walks the prototype chain**. A routed model id of `constructor` or +`toString` returns an `Object.prototype` function, which makes +`buildBehaviorFingerprintV1` throw "unsupported value type function". That +throw is swallowed by `resolvePassiveRouteSubjectId`, so the subject is +silently dropped — inside a linker whose contract says implementations do not +throw. `openai-responses.ts` already guards `modelPreferHostedTools` for +exactly this reason. + +## Per-PR verdicts + +| PR | Site | Verdict | +|---|---|---| +| #2085 | admission input ceiling | **merge** | +| #2086 | `ocx models` CLI | **merge** (draft; ready on content) | +| #2100 | routing capability evidence | **hold** — incomplete migration | +| #2077 | Lab behavior fingerprint | **hold** — over-broad migration | + +No two touch the same file or function, so there is no textual conflict; the +order is about correctness, not merge mechanics. + +### #2085 — merge + +The "definite wrong answer" claim is verified: a missed `modelContextWindows` +lookup falls through to the provider-wide `contextWindow` +(`src/server/responses/input-admission.ts:136`), so the admission gate refuses +turns the model can hold. Both per-model reads in the file are migrated. + +### #2086 — merge + +The ordering claim checks out against `src/vision/index.ts:29`: +`isModelTextOnly` returns true on the `noVisionModels` match before it reads +the modality map, and the PR mirrors that order. It also correctly upgrades +`.includes(model)` to `modelInList`. The description is stale (says two tests, +adds three). + +### #2100 — hold + +The six map reads are migrated correctly, but the **`noVisionModels` +precedence is missing**. With `noVisionModels: ["gpt-oss"]` and +`modelInputModalities: {"gpt-oss:120b": ["text","image"]}`, the runtime says +text-only while `candidateCapabilityEvidence` reports `image: true`. Routing +acts on this evidence, so it can select a candidate for image work that +execution then rejects — the exact ordering bug #2086 fixes on the CLI surface, +left unfixed on the routing surface. + +Needs: the no-vision check before modality derivation, plus a regression for +the conflicting-evidence case. Also `contextWindow.not.toBe(8_000)` is a weak +assertion — it accepts `undefined` and any other wrong value. + +### #2077 — hold + +The prototype-chain defect is real and the fix is right for the nine +family-aware maps. But `modelValue` is **also** used for +`modelPreferHostedTools` (`src/routing/compatibility/behavior.ts:186`), so the +PR makes a `gpt-oss` family entry affect `gpt-oss:120b` in the behavior +fingerprint even though the adapter will not apply it. That violates the +contract it is trying to enforce. `modelOpenRouterRouting` at `behavior.ts:71` +is still a bare read and was missed. + +Needs: `modelRecordValue` for the nine family-aware maps, and a separate +exact-own helper for hosted tools and OpenRouter routing. + +One correction to the PR's own narrative, worth passing to the author: the +throw is caught at `src/routing/compatibility/subject.ts:125`, not by +`resolvePassiveRouteSubjectId`. The silent-subject-drop conclusion holds; the +described control flow does not. + +## Recommended order + +1. `#2085` +2. `#2086` (may swap with #2085) +3. `#2100` after no-vision precedence lands +4. `#2077` after exact-own semantics are preserved + +## Exit criteria + +- `c-mrv`: one shared contract verdict plus four per-PR verdicts, each from a + read of the actual diff. **Met** — and the contract itself was corrected by + the review rather than merely confirmed. +- No merges. diff --git a/devlog/_plan/260819_next_roadmap/041_r4_posted_verdicts.md b/devlog/_plan/260819_next_roadmap/041_r4_posted_verdicts.md new file mode 100644 index 0000000000..093e3618aa --- /dev/null +++ b/devlog/_plan/260819_next_roadmap/041_r4_posted_verdicts.md @@ -0,0 +1,40 @@ +# 041 — R4 verdicts as posted + +| PR | Verdict | Comment | +|---|---|---| +| #2085 | merge | [5340681951](https://github.com/lidge-jun/opencodex/pull/2085#issuecomment-5340681951) | +| #2086 | merge (draft) | [5340682189](https://github.com/lidge-jun/opencodex/pull/2086#issuecomment-5340682189) | +| #2100 | hold | [5340682460](https://github.com/lidge-jun/opencodex/pull/2100#issuecomment-5340682460) | +| #2077 | hold | [5340682748](https://github.com/lidge-jun/opencodex/pull/2077#issuecomment-5340682748) | + +Each comment says it was reviewed as part of a four-PR batch, so an author +seeing a hold knows it came from a comparison rather than a one-off objection. + +## What each author was asked for + +**#2100** — check `noVisionModels` before deriving modalities, plus a +regression for the conflicting-evidence case. The framing matters: this is a +gap in the PR's own terms, since its stated goal is that the evidence agree +with the resolver it describes. Also flagged `contextWindow.not.toBe(8_000)` +as a weak assertion that accepts `undefined`. + +**#2077** — split the migration: `modelRecordValue` for the nine family-aware +maps, an exact-own helper for `modelPreferHostedTools` and +`modelOpenRouterRouting`. The PR currently makes a family entry affect the +behavior fingerprint for a map the adapter reads exactly — the same divergence +it set out to remove, pointed the other way. Also corrected the description's +control flow (the throw is caught at `subject.ts:125`, not by +`resolvePassiveRouteSubjectId`), since that text would otherwise land in the +commit message. + +**#2085 / #2086** — merge verdicts, with one note each that is not a change +request: #2085's direct `modelRecordValue(...)` assertions are ground truth +rather than coverage, and #2086's description undercounts its own tests. + +## Why the batch was worth it + +The shared contract had to be corrected before any verdict was safe. As first +written it implied every per-model map should move to `modelRecordValue`. Two +maps are deliberately exact-own-only, so that migration is a regression for +them — and #2077 performs exactly that migration. Reviewed one at a time, +#2077 reads as a correct one-line fix with a good test. diff --git a/devlog/_plan/260819_next_roadmap/050_execution_ledger.md b/devlog/_plan/260819_next_roadmap/050_execution_ledger.md new file mode 100644 index 0000000000..ca72bb0ae8 --- /dev/null +++ b/devlog/_plan/260819_next_roadmap/050_execution_ledger.md @@ -0,0 +1,342 @@ +# 050 — Execution ledger + +Append-only record of what each work-phase actually did, with the evidence that +establishes it. A claim without an entry here did not happen. + +Loop: HOTL, session `01a01949`, goalplan +`drain-the-opencodex-pr-queue-in-reviewable-order`. + +## Standing constraints + +- **One merge lane only.** `#2084` then `#2089`. Every other PR in this loop is + review, rebase, or retarget. +- **Force-push is limited to branches we own** (`codex/split-*`, + `codex/tmp-reclaim-*`). Contributor fork heads are never rewritten; their + bases are retargeted with `gh` instead. +- **R5 (the split program proper) is out of scope.** WP1/WP1b/WP2a get rebased + so they stop rotting; no split train starts. + +## wp0 — docs-first roadmap cycle + +Outcome: **DONE.** + +| Item | Evidence | +|---|---| +| Decade docs written | `000_roadmap.md`, `010`, `020`, `030`, `040` | +| Committed | `015f119d5` | +| Audit corrections | `f94cbda63` | +| Audit lane | sol-medium read-only agent `01a01963` | + +### What the audit changed + +Six load-bearing claims were sent to an independent lane. Four came back +CONFIRMED; two came back PARTIAL, and both PARTIALs were real errors in the +first draft, not quibbles. + +**010 had the stale-base mechanism backwards.** The first draft said CI ran +"dev's newer test against the PR's older source." Run `32130164359` shows the +opposite: the *test* held the old two-argument assertion and the *source* held +the new three-argument call from `91979cf14`. `6c0bde453` fixed the assertion +afterwards. The conclusion (merge skew, not a defect) survived; the stated +mechanism did not. The draft also undercounted the failing legs — six, not +four — and called `#2023` "fully green" when `hygiene` and `enforce-target` +fail on it. + +**030 overstated how reachable `#2062`'s fail-open is.** `#2056` adds +`shortPercent` to `hasKnownQuotaValue`, so short-only snapshots enter the valid +cache; `#2062` does not, so its short-only parses return `null` and the +fail-open needs disk hydration or direct cache insertion to reach. Narrower, +not absent. The audit also found a `#2062`-only defect the draft missed: a +later partial snapshot drops the preserved short tuple. + +**One risk nobody had flagged:** `#2102`'s sanitizer is called outside the +`if (forward)` branch, so the chosen `prompt_cache_retention` fix also strips +the field from API-key and third-party `openai-responses` passthroughs. That is +defensible for genuine OpenAI endpoints and untested for custom ones. + +The lesson worth carrying: the draft's *conclusions* held up, and its +*explanations* did not. An explanation that survives because its conclusion is +right is still wrong, and it is exactly the kind of wrong that gets copied +forward into the next document. + +## wp1 — R2 merge temp-reclaim stack + +Outcome: **DONE.** Both PRs merged after three confirmed review findings were +fixed. + +| Step | Evidence | +|---|---| +| #2084 merged | `973258488`, ancestor of `origin/dev` | +| #2089 retargeted to `dev` | diff became the 9 phase-2 files only | +| #2089 merged | `c4bf833c9`, ancestor of `origin/dev` | +| Checks at merge | zero FAILURE, zero PENDING on both exact heads | +| Post-merge CI | runs `32241217016` and `32241261180` on the merge SHAs | +| Branches deleted | both, after the retarget (never before) | + +A pre-merge review lane (sol-medium, agent `01a0196e`) returned +**DO-NOT-MERGE** on the original heads. Three of its findings were confirmed +against the code and fixed; the blocking one was adjudicated down and is +recorded here rather than silently dropped. + +### Fixed on `codex/tmp-reclaim-1-sweeper` (`1fbac66f8`) + +**Directory handle leak on every truncated scan.** `list` is a generator that +closes its handle in a `finally`, but the consumer drove it with manual +`iterator.next()` calls and left the loop with `break`. A `finally` does not +run when a consumer simply stops calling `next()` — only `return()` resumes the +generator to completion. The periodic reclaim truncates *by design* (entry cap, +cleanup cap, 25 ms deadline), so this leaked one handle per truncated tick, +every minute, on exactly the slow filesystems the deadline exists for. +Every early exit now routes through a `stopScan()` that calls +`iterator.return()`. + +**The deadline test was vacuous.** Its fake clock started at `0` while the +fixtures carried real epoch mtimes, so every computed age was negative and the +files survived the 15-minute grace whether or not a deadline check existed — +the test passed against its own ablation. The clock is now anchored to real +time and the test carries an explicit unbounded-run assertion, so the deadline +is the only reason nothing is removed. + +Both fixes were **driven red**: reverting `stopScan()` fails the new closure +test and nothing else; deleting the deadline check fails the repaired deadline +test and nothing else. + +### Fixed on `codex/tmp-reclaim-2-doctor` (`e298cf8ea`) + +**The budget warning could never print.** It keyed on +`eligible > removed + failed`, but outside a dry run an entry is counted +eligible and then unlinked or failed on the same iteration, so those two are +always equal. An operator whose backlog exceeded the 4096-file budget was told +the reclaim had finished. The scan now carries an explicit `truncated` flag, +set wherever the loop stops on a budget rather than on the end of the +directory, OR-ed across swept directories. The dry-run report is bounded by the +entry cap too, so a truncated report now says its count is a floor. + +The partial-reclaim test asserted a state production cannot reach +(`eligible: 816, removed: 512`); it now uses a reachable one and is paired +with an ablation guard. Driven red: restoring the old comparison fails it. + +Verification: 174 pass / 0 fail across `doctor`, `responses-state`, and +`state-store-sweeper`; `tsc --noEmit` clean. + +### Adjudicated, not fixed + +**The reviewer's stated blocker — the boot floor can unlink a live writer's +temp — is real but narrower than "blocking".** When `predatesBoot` is true the +liveness probe is genuinely skipped. But reaching it requires a writer that has +been stalled past the 15-minute grace *and* whose temp mtime predates this +machine's boot. On a single host that is self-contradictory: a process running +now cannot have written before the boot it is running after. The scenario needs +a config dir shared across hosts or containers — which the code comment already +names as the case where the computed boot can be wrong. + +Left as-is deliberately: the alternative is to gate the floor on +single-host ownership, which needs a durable host identity we do not have. The +comment documents the limit honestly. Revisit if shared-config-dir deployments +become supported rather than incidental. + +**Two smaller findings deferred with reasons.** (a) An aliased config dir +(literal and resolved paths pointing at one directory through a symlink) makes +the `Set` hold two strings for one directory, so a dry run double-counts. +Cosmetic, and the fix is a `realpath` dedupe worth doing with a test that can +build the alias. (b) `resolveWriteTarget` follows a snapshot symlink out of the +config dir, so scanning follows it too. That is the intended dotfiles-managed +behavior; containment would be a separate design decision, not a fix. + +## wp4 — R4 modelRecordValue batch review + +Outcome: **DONE (review only, no merges).** Lane: sol-medium agent `01a01979`. + +The review did not merely confirm the batch premise — it **refuted the shared +contract as originally written**, which is the whole reason this lane was worth +running. The draft implied every per-model map should migrate to +`modelRecordValue`. Two maps (`modelPreferHostedTools`, +`modelOpenRouterRouting`) are deliberately exact-own-only, so migrating them +adds family and case-folded inheritance the adapter will not honor. The +invariant is "read it the way the runtime reads *that map*"; `modelRecordValue` +is only its implementation for the family-aware set. + +Verdicts: `#2085` merge, `#2086` merge, `#2100` hold (missing +`noVisionModels` precedence lets routing pick a candidate for image work that +execution rejects), `#2077` hold (over-broad migration reaches +`modelPreferHostedTools`; `modelOpenRouterRouting` still read raw). + +Full reasoning in `040_r4_modelrecordvalue_batch.md`. + +Verdicts posted to the PRs: + +| PR | Comment | +|---|---| +| #2077 | `5340642374` | +| #2085 | `5340642645` | +| #2086 | `5340642926` | +| #2100 | `5340637836` | + +A first attempt at the #2100 comment was posted through a shell argument and +the backticks in it were evaluated by zsh, stripping every code span. It was +deleted and reposted from a file. Worth remembering: PR bodies full of +identifiers go through `--body-file`, never `--body`. + +## wp2 — R1 rebase split stack + +Outcome: **DONE — and the stale-base hypothesis is now measured, not assumed.** + +| PR | Old head | New head | Rebase | +|---|---|---|---| +| #2019 WP1 | `194f9f2a9` | `35990f6ea` | clean, no conflicts | +| #2023 WP1b | `b2ac2500c` | `874598bd3` | **recut**, see below | +| #2036 WP2a | `7561e5551` | `6c6925a4d` | clean, no conflicts | + +`#2019` and `#2036` rebased without a single conflict, which is itself +evidence for the stale-base reading: 102 and 42 commits of drift produced zero +textual disagreement. + +### The claim 010 refused to assert, now proven + +`010` deliberately said the stale-base diagnosis was the hypothesis the rebase +would *test*, not an established fact, because old CI on a sibling PR cannot +prove a rebased head is clean. The test has now run. On `#2019`'s rebased head +(run `32241365996`): + +``` +test 1/4 pass test 2/4 pass +test 3/4 pass test 4/4 pass +gates pass macos-launchd pass +``` + +Every leg that was red before is green after, with **no source change** — the +same extraction, replayed onto current `dev`. Six failing legs to zero. The +extraction was never broken; the base was. + +### WP1b was recut, not rebased — and that is the honest description + +The rebase conflicted across the entire file. The reason is structural rather +than semantic: WP1b rewrites `types.ts` from 1884 lines into a 103-line barrel, +so *any* dev commit that adds a declaration to the old file collides with the +rewrite everywhere. Three conflict hunks spanning lines 1-3450 is what "the +file was replaced" looks like to a three-way merge. + +Resolving hunk-by-hunk would have been guesswork. Instead the leaves were +re-applied onto the rebased parent and the actual dev delta was re-homed +deliberately. That delta was exactly three declarations: + +| Declaration | Origin | New home | +|---|---|---| +| `OcxReasoningReplayIdentity.credentialDurableIdentity` | #2078 | `src/types/request.ts` | +| `CodexAccount.planSource` | dev | `src/types/accounts.ts` | +| `CodexAccount.planCredentialGeneration` | dev | `src/types/accounts.ts` | + +Taking "ours" on that conflict would have silently dropped all three. Verified +after: `tsc --noEmit` clean, 150 tests pass, `types.ts` at 103 lines. + +### CI proof on the new heads + +| PR | New head | Cross-platform CI | +|---|---|---| +| #2019 | `35990f6ea` | run `32241365996` **success** | +| #2023 | `874598bd3` | run `32241478125` **success** | +| #2036 | `6c6925a4d` | run `32241513290` **success** | + +On `#2019` the six legs that were red before the rebase — `test 1/4` through +`test 4/4`, `gates`, and `macos` — all pass on the new head with no source +change other than the rebase. 010 called stale base the hypothesis this rebase +would test rather than an established fact; it held. + +The mechanism is confirmed at the line level: on `35990f6ea` the assertion at +`tests/codex-app-server-processes.test.ts:393` and the call at +`src/cli/dispatch.ts:246` now both carry the three-argument +`invalidateCodexModelsCacheWithPermit(permit, owningCodexHome, { allowWhenDesiredDisabled: true })` +form. They disagreed only because CI merged an old head against a newer base. + +### Independent check for silent loss + +"Exactly three fields" was a claim about a recut, not a guarantee, so a +separate lane (`01a01988`) compared every exported name and every interface +field between `origin/dev` and the new head: + +``` +EXPORT_COUNTS dev=85 leaf_declarations=85 barrel_reexports=85 +DEV_MISSING_FROM_LEAVES (none) DEV_MISSING_FROM_BARREL (none) +ALL_INTERFACE_COUNTS dev=53 head=53 +ALL_MISSING_FIELDS 0 ALL_EXTRA_FIELDS 0 +``` + +It confirmed the delta was exactly the three claimed fields, and that +`#2019`/`#2036` are patch-identical to their pre-rebase series by +`git range-diff`. Worth keeping for the rest of the split program: a +name-level audit alone would miss a dropped field inside a preserved +interface, which is the failure mode a barrel extraction actually risks. + +### The hygiene gate still fails, correctly + +All three still fail `hygiene: missing_regression_test` and `enforce-target`. +The first is right and is not fixed by rebasing: a pure-move PR changes `src/` +without changing a test. The honest resolution is `test-exception-approved` — +the oracle for a barrel extraction is the ~400 files that import through it +plus `tsc`, and a test asserting "the barrel re-exports `OcxTool`" restates +the compiler. + +## wp3 — R3 collisions and retargets + +Outcome: **DONE** (decisions recorded, retargets applied, no merges). + +All eight wrong-branch PRs retargeted `main` -> `dev`: #2110, #2109, #2099, +#2082, #2063, #2062, #2032, #2029. Seven are `MERGEABLE` after the retarget; +#2063 is `CONFLICTING` and overlaps the already-merged #2055, so it needs an +author rebase and a rescope. No contributor head was rewritten — every one of +those heads lives in a fork. + +`prompt_cache_retention` (#2092): **#2102 wins.** Comment posted there with +the reasoning and the one pre-merge request (its sanitizer sits outside the +`if (forward)` branch, so it also touches API-key and third-party passthroughs +and needs an API-key regression). #2091 and #2099 told why they were not +chosen rather than closed silently. + +K12 (#2047): **neither #2056 nor #2062 merges.** Same root cause posted on +both, with the asymmetry named — #2062 is narrower on reachability, #2056 is +ahead on preservation, both carry the scoring fail-open. + +### Final retarget state (verified at close) + +| PR | Base | Mergeable | +|---|---|---| +| #2110 | `dev` | MERGEABLE | +| #2109 | `dev` | MERGEABLE | +| #2099 | `dev` | MERGEABLE | +| #2082 | `dev` | MERGEABLE | +| #2063 | `dev` | **CONFLICTING** | +| #2062 | `dev` | MERGEABLE | +| #2032 | `dev` | MERGEABLE | +| #2029 | `dev` | MERGEABLE | + +All eight are drafts, which is the contributor-PR default and not a problem to +solve here. `#2063` is the one that needs its author: it conflicts and overlaps +`#2055`, which already merged as a partial fix for the same issue, so it needs +a rescope rather than a mechanical rebase. + +## Loop close + +Terminal outcome: **DONE.** Five work-phases, ten criteria, all carrying +evidence. + +One merge lane was authorized and one was used. Everything else in this loop +was review, rebase, or retarget — which is what the scope asked for, and worth +stating plainly because a queue-drain loop is exactly where scope creep would +be easiest to justify after the fact. + +### What the review lanes actually bought + +Three independent lanes ran. None of them merely agreed: + +- The **roadmap audit** caught the stale-base mechanism stated backwards, an + undercounted failure set, and a "fully green" claim that was not. +- The **pre-merge review** returned DO-NOT-MERGE and found a directory-handle + leak on every truncated scan, a budget warning that could never print, and a + deadline test that passed against its own ablation. All three were confirmed + in code and fixed before the merge. +- The **batch review** refuted the shared contract the batch was built on. + +The pattern across all three: the *conclusions* in the first drafts held up and +the *explanations* did not. An explanation that survives because its conclusion +happens to be right is still wrong, and it is the kind of wrong that gets +copied into the next document unchallenged. diff --git a/devlog/_plan/260819_next_roadmap/060_outcome.md b/devlog/_plan/260819_next_roadmap/060_outcome.md new file mode 100644 index 0000000000..324b9be073 --- /dev/null +++ b/devlog/_plan/260819_next_roadmap/060_outcome.md @@ -0,0 +1,69 @@ +# 060 — Campaign outcome + +Loop: HOTL, session `01a01949`, five work-phases. All four requested lanes +closed. The split program (R5) was not started, as scoped. + +## What shipped + +| Lane | Outcome | +|---|---| +| R2 merge | #2084 (`973258488`) and #2089 (`c4bf833c9`) merged to `dev`, in that order | +| R1 rebase | #2019, #2023, #2036 rebased/recut and pushed; **all three Cross-platform CI green** | +| R3 collisions | `prompt_cache_retention` decided for #2102; K12 held on both PRs with a shared root cause; eight wrong-branch PRs retargeted | +| R4 review | four verdicts posted: merge #2085, merge #2086, hold #2100, hold #2077 | + +Two source defects were found and fixed before the only authorized merge, and +two plan documents were corrected by their own audit lanes. Those four events +are the substance of this campaign; the merges and rebases are the mechanics. + +## The four things worth remembering + +**1. A plan's conclusion can be right while its explanation is wrong.** +010 first said CI ran "dev's newer test against the PR's older source." The run +log shows the reverse — newer source, older test. The conclusion (stale-base +skew) survived and was later proven by a green CI run on the rebased head. The +mechanism did not. An explanation that survives because its conclusion happens +to be right is exactly the kind that gets copied into the next document +unchallenged. + +**2. Two of the defects we shipped fixes for were invisible to green CI.** +The directory-handle leak fired only on truncated scans; the budget warning was +dead code that no test exercised because no test could produce the state. Both +PRs were green before review. Green CI proved the code did not break anything +it already tested — nothing more. + +**3. A test can pass against its own ablation.** The deadline test set its fake +clock to `0` while fixtures carried real epoch mtimes, so every computed age +was negative and the files survived the 15-minute grace whether or not a +deadline check existed. Deleting the feature it guarded did not fail it. Every +repaired guard here was driven red once against a deliberate violation, and the +two new ones carry explicit ablation assertions. + +**4. Batch review earns its keep by refuting its own premise.** R4's contract +("read per-model overrides the way the runtime reads them") implied migrating +every map to `modelRecordValue`. Two maps are deliberately exact-own-only, so +that migration is a regression — which is what #2077 does. Four separate +reviews would each have seen a correct-looking one-line change. + +## Left open, deliberately + +- **The three split PRs are green but still gated** on + `hygiene: missing_regression_test`. That gate is correct for a pure-move PR; + the honest resolution is `test-exception-approved`, not a test that restates + the compiler. Needs a maintainer decision, not more code. +- **#2100 and #2077** need author changes named in their review comments. +- **#2056 / #2062** both need the same scorer gate before either can land. +- **#2063** is `CONFLICTING` and overlaps the merged #2055; it needs an author + rebase and a rescope. +- **The boot-floor limitation** in the reclaim path is documented in code and in + 020: it can skip the liveness probe for a temp older than this boot, which is + only reachable when a config dir is shared across hosts. Revisit if that + becomes a supported deployment rather than an incidental one. +- **Two smaller reclaim findings** deferred with reasons in 050: aliased-directory + double counting in the dry run, and symlink containment on the directory side. + +## R5 remains unstarted + +WP1/WP1b/WP2a are now rebased onto current `dev` with green CI, so the split +program's opener is no longer rotting. Nothing past it was touched: no WP2b +stateful config train, no registry, no service, no `responses/core.ts` waves. diff --git a/devlog/_plan/260819_next_roadmap/070_next_roadmap_split_and_dogfood.md b/devlog/_plan/260819_next_roadmap/070_next_roadmap_split_and_dogfood.md new file mode 100644 index 0000000000..e13bc1fb0b --- /dev/null +++ b/devlog/_plan/260819_next_roadmap/070_next_roadmap_split_and_dogfood.md @@ -0,0 +1,355 @@ +# 070 — Next roadmap: bug PRs, the split merge, and the dogfooding gate + +Written 2026-08-19 after the queue-drain campaign closed. State at writing: +56 open PRs (17 `review-ready`), 24 open `bug` issues, `dev` at `c4bf833c9`, +npm `latest=2.26.0` / `preview=2.26.0-preview.20260819`. + +## The question this answers + +Merging a mega-file split is not like merging a fix. A fix either works or +fails on the path it touches. A split touches **no path and every path at +once**: nothing changes behaviorally, so nothing fails visibly, and the way it +breaks is by dropping something the type system or a test never looked at. + +So the ordering below is not "important things first". It is **cheapest-to- +verify first, and the split only once the queue behind it is short enough that +a rebase storm is affordable.** + +## Fact 1: the three split PRs are not one risk class + +This is the single most useful thing measured, and it reorders everything. + +| PR | `src` diff | Runtime code moved | Real risk | +|---|---|---|---| +| #2019 WP1 | 3 files, +184/-162 | 7 value helpers (`namespacedToolName`, `modelInList`, wire pins) | **low** | +| #2023 WP1b | 5 files, +1801/-1720 | **none — type-only** | **near zero** | +| #2036 WP2a | 4 files, +29/-25 | 2 functions + 2 import sites | **low, but it is the one that touches routing** | + +`#2023` looks like the scariest PR in the repository (1801 insertions, +1720 deletions) and is the safest thing in this document. Every line it moves +is erased at compile time. If the barrel is wrong, `tsc` fails; there is no +runtime state in which it can be subtly wrong. The independent audit already +confirmed 85/85 exports and 53/53 interfaces with zero field drift. + +`#2036` is 29 lines and is the only one that changes what `src/router.ts` and +`src/routing/profile.ts` import at runtime. + +**Corrected after review: the TDZ framing was overstated.** The cycle is real, +but the bindings crossing it are *function declarations*, which hoist — there +is no demonstrated top-level access that could hit a temporal dead zone. And +`#2036` **removes** the cycle rather than introducing one; the new leaf has +zero imports. Its CI, npm-global smoke, and startup checks are already green at +its exact head. Calling it "the riskiest split" was not supported. + +What is still true, and is the reason to isolate it: it is the only one of the +three whose failure mode would be a **startup** failure rather than a compile +failure, and startup ordering is the thing the suite structurally cannot +observe — every test imports a fully-warm module graph. That justifies its own +soak window. It does not justify calling it dangerous. + +The cycle is real, not hypothetical. On `dev`: + +``` +src/config.ts:38 import { routingProfileIssues } from "./routing/profile"; +src/routing/profile.ts:16 import { hasOwnProvider } from "../config"; +``` + +`#2036` cuts the return edge by moving `hasOwnProvider`/`isValidProviderName` +into `src/config/provider-name.ts` and repointing `profile.ts` and `router.ts` +at the leaf. That is the correct fix and the risk assessment named it as the +prerequisite for the rest of WP2. It is still the one to isolate, because +"which module finished initializing first" is not something the test suite +observes — every test imports a fully-warm module graph. + +### Revised risk ranking + +`#2019` moves actual runtime bindings (7 functions plus module-level constants +including `MODEL_ADAPTER_OVERRIDE_ALLOWED` and the wire-pin table). `#2036` +moves 2 functions and removes a cycle. `#2023` moves nothing executable. + +So the honest ordering by *runtime* exposure is +**#2019 > #2036 > #2023** — which happens to match the ancestry-forced merge +order for the first two. The plan's soak windows follow exposure, not size. + +**Correction (caught in review of this document's own first draft).** The first +version of this section said "merge #2023 first because it is type-only". That +is impossible: `#2023`'s base is `codex/split-wp1-types`, and +`git merge-base --is-ancestor` confirms `#2023` *contains* `#2019`. A stacked +child cannot land before its parent. Risk ranking does not get to override +ancestry. + +**Actual merge order: #2019 -> #2023 -> #2036.** + +The risk finding still changes something real, just not the order: it changes +**where the soak windows go**. `#2019` and `#2023` are one logical unit (the +parent is 7 pure-function moves, the child is type-only) and can share a +window. `#2036` gets its own, because it is the only one that touches module +init. + +## Fact 2: the rebase storm is 10 PRs, not 6 — the first count was wrong + +**Corrected after review.** The first draft sampled 11 PRs I guessed were +likely and reported 6 hits. Enumerating every open PR gives 13 that touch +`src/types.ts` or `src/config.ts` — 10 once the three split PRs themselves are +excluded. The draft missed **#2112, #1829, #1645, #1624** and undercounted the +conflicting ones. + +| PR | Mergeable | Touches | Note | +|---|---|---|---| +| #2112 | MERGEABLE | types | **bug PR** (#2106 candidate), draft | +| #2080 | MERGEABLE | config | review-ready, FastWire B2 | +| #2054 | CONFLICTING | types | +1683, already conflicting | +| #2050 | MERGEABLE | types | +11559, 63 files | +| #1934 | MERGEABLE | types | **bug PR**, draft | +| #1905 | CONFLICTING | both | already conflicting | +| #1829 | MERGEABLE | config | +2237 | +| #1747 | CONFLICTING | both | +4548, 87 files | +| #1645 | MERGEABLE | types | vision sidecars | +| #1624 | CONFLICTING | both | already conflicting | + +**Four are already CONFLICTING** (#2054, #1905, #1747, #1624), not two. And the +draft's inference from that was wrong too: "already conflicting means the split +costs them nothing" is false. An existing text conflict does not pre-pay for +structural drift — those branches still have to be reconciled against module +paths that will not exist in the form they were written against. + +**Consequence: the storm is real but still not a reason to wait for the whole +queue.** Six MERGEABLE PRs pay a genuine cost (#2112, #2080, #2050, #1934, +#1829, #1645). Of those, the two worth landing first are the ones that are both +small and close to ready: **#2080** (review-ready) and **#1934** (bug fix). The +rest are large or draft and will need author work regardless. + +## Fact 3: we already have a dogfooding channel and are not using it as a gate + +`preview` publishes to npm under the `preview` dist-tag +(`release.yml` enforces `*-preview.*` versions on that branch). Right now +`preview` is **14 commits behind `dev` and 30 ahead** — it is a release +artifact, not a soak channel. + +The split is exactly the change class where a soak channel earns its cost: no +test will catch a dropped optional field that nothing reads yet, but a week of +real traffic will. + +**Consequence: preview becomes the split's gate.** Not for bug fixes — those +keep going straight to `dev`. + +### Two operational details the first draft skipped + +**Cutting a preview is a release operation, not a tag.** `release.yml` is +`workflow_dispatch` only, allows exactly `main`/`latest` or +`preview`/`preview`, and requires a `*-preview.*` version in the dispatched +`preview` checkout. `scripts/release.ts` automates the ceremony but **commits +and pushes** — there is no dry-run rehearsal. And `preview` is currently 14 +behind / 30 ahead of `dev`, so "cut a preview of C1+C2" means first +reconciling a divergent branch, not fast-forwarding it. Budget that as a step. + +**Freeze the candidate or lose attribution.** The plan's claim that `#2036` +gets its "own blame surface" is only true if the preview cut for it contains +*it* and not a week of unrelated bug merges. Each soak window must name an +exact SHA and state what else rode along. If continuous bug merging makes that +impossible, the honest options are a short `dev` freeze around the split cuts +or an explicit admission that attribution is shared — not a claimed isolation +the history does not support. + +## The roadmap + +### Phase A — clear the cheap queue (no split work) + +Merge order among `review-ready`, smallest blast radius first: + +1. **#2085** (admission window) — 44 lines, verdict already posted, merge. +2. **#2086** (`ocx models` CLI) — flip from draft, merge. +3. **#2102** (`prompt_cache_retention`) — after the API-key regression we + asked for. Then close #2091 and #2099 as superseded. +4. **#2035** (Google reasoning tiers), **#2031** (MiMo vision sidecar), + **#1878** (docs) — small, independent, review-ready. +5. **#2105** (Claude shell hook), **#2103** (xAI tool schema) — review-ready, + one subsystem each. + +Deliberately **not** in phase A: #2101 (1397 lines, account entitlement — needs +its own security-adjacent review), the Antigravity stack #2068-#2071 (~5600 +lines, one author, needs a dedicated lane), #2072/#2075/#2080 FastWire +(#2072 already has an unresolved assumed-tier billing finding). + +### Phase B — land the two PRs the split would inconvenience + +**#2080** and **#1934**. Both touch a split target; both are cheaper to land +now than to rebase later. #2080 is review-ready; #1934 is draft and needs the +author. + +If either stalls more than a few days, drop it from this phase rather than +letting it hold the split. The storm cost for two import-line rebases is +lower than the cost of the split rotting again. + +### Phase C — the split merge, one PR per soak window + +This is the part that needs the discipline. + +**C1. #2019 (WP1, value helpers) -> dev.** +Parent of the stack; must land first. The 7 moved helpers are pure functions +with no module state. Post-merge check: grep for duplicate declarations — the +"singleton forking" risk from the original risk assessment does not apply to +pure functions, but the habit should start on the cheapest PR, not the +dangerous one. + +**C2. #2023 (WP1b, type-only) -> dev**, after retargeting from +`codex/split-wp1-types` to `dev` (the parent branch is deletable only after +that retarget — deleting the base of an open PR closes it, which this campaign +already relearned on #2089). + +Requires: `test-exception-approved` from a maintainer. The hygiene gate is +right that `src/` changed without a test, and the honest answer is that a +barrel's oracle is `tsc` plus the 396 test files that import through it — a +test asserting "the barrel re-exports `OcxTool`" restates the compiler. The +exception label exists in `pr-hygiene.yml` for exactly this. + +Verification beyond CI: re-run the export/interface parity audit against the +merge commit, not the PR head. That audit is now the standing check for every +remaining split PR — a name-level check would miss a dropped field inside a +preserved interface, which is the only way a barrel extraction can hurt. + +**C3. Cut a preview release containing C1+C2. Soak 5-7 days.** +This is the first real dogfooding gate. See the section below for what +"soak" means concretely. + +**C4. #2036 (WP2a, config leaf) -> dev, alone.** +Do not bundle it with C1/C2. Not because it is dangerous — the review showed it +is not — but because it is the only one whose failure mode is **startup** +rather than compile, and startup ordering is what the suite structurally cannot +observe. A window where it is the only module-graph change is the cheapest way +to attribute a "the proxy will not start" report if one arrives. Freeze around +the cut, or state plainly what else rode along. + +**C5. Second preview. Soak. Then promote to `main`/`latest`.** + +### Phase D — WP2b onward, only after C5 is clean + +The stateful config train (schema + load + mutation + live-rebase, which the +risk assessment says must move together or not at all) is the first genuinely +dangerous work package: eight module-level singletons, including a SQLite +mutation lock and three WeakMaps keyed on config object identity. Forking any +one of them is a silent correctness bug. + +Do not start it until a preview carrying C1-C4 has soaked without a +split-attributable report. If phase C produces even one, the answer is to fix +the mechanism that let it through before adding a harder package. + +## What "dogfooding" has to mean here, concretely + +A soak that only checks "did anyone complain" cannot distinguish a clean split +from an unexercised one. Three things make it a real gate: + +**1. The maintainer's own `ocx` runs the preview build** — the published +tarball, installed the way a user installs it, not the dev checkout. + +**Correction to the first draft's justification.** It argued the risk was a +missed `src/types/*.ts` file causing a module-not-found in the published +package. That is not credible: `package.json` ships `src` **wholesale** +(`files: ["bin","src",...]`), `.npmignore` does not exclude it, and the current +published preview tarball contains 724 `src/` entries. A file committed under +`src/` is shipped; a file not committed fails CI first. And for `#2023` +specifically the references are type-only and erased — there is no runtime +resolution to fail. + +The honest reason to run the published build is narrower and applies to +**`#2019` and `#2036`**, which do move runtime bindings: it exercises the +packaged module graph and real startup, which `npm-global-smoke` (install only) +does not. + +**For `#2023` the runtime soak proves nothing at all**, and the plan should not +pretend otherwise. Erased interfaces cannot fail a routed turn. Its real risk is +a **type-contract** regression — a dropped optional field, a widened union, an +interface a downstream consumer no longer satisfies — and the gate for that is +the export/interface parity audit plus `tsc` against the merge commit, which is +exactly what this campaign already built and ran. + +**2. Named surfaces get exercised, not just "used for a while".** The split +moves types used by routing, config, providers, and accounts. A soak that only +runs one provider on one model proves nothing about `OcxProviderConfig` or +`OcxComboConfig`. Minimum exercise set per soak window: + +- one Codex-account (native) turn and one API-key provider turn, +- one routed/combo turn (exercises `OcxComboTarget`, `OcxRoutingProfileConfig`), +- one vision or image turn (`OcxImageContent`, sidecar config), +- `ocx doctor`, `ocx models`, and a dashboard load (the config and catalog + types), +- one proxy restart (module init order — this is the C4 gate specifically). + +**3. A dated `NO-REPORT` line is written down.** "Nothing broke" that is not +recorded is indistinguishable from "nobody looked". Each soak window closes +with a line in this unit naming the version, the dates, and which surfaces +were exercised — or naming the report and its disposition. + +**Explicitly not a gate:** green CI on the merge commit. That is necessary and +it is already automatic. The whole point of the soak is the class of defect CI +cannot see, and this campaign produced two of those (a directory handle leak +on truncated scans, a warning branch that could never fire) in code that was +green. + +## Bug PRs and issues — where they sit in all this + +**Corrected: "never gated on the split" was false.** Two of the overlapping +PRs are themselves bug fixes — **#2112** (the #2106 `code_mode_only` candidate) +and **#1934** (namespaced tool aliases). Any overlapping PR that has not landed +before the split must be rebased or re-cut against leaves that did not exist +when it was written. That is gating, whatever we call it. + +The accurate statement: **most** bug work is independent of the split, and the +two that are not should land in phase B alongside #2080. Everything else flows +continuously. + +Standing bug work, in rough priority: + +| Issue | Why it ranks | +|---|---| +| #2107, #2108 (Windows/WSL 502, native-main gate stuck) | user-visible breakage on a supported platform; #2108 needs a restart to clear | +| #2097 (unentitled accounts advertised) | routing sends traffic to accounts that will refuse it; #2101 is the candidate fix but is 1397 lines | +| #2092 (`prompt_cache_retention`) | decided, waiting on #2102 | +| #2047 (K12 short window) | decided as "neither PR merges"; both need the scorer gate | +| #1852 (PowerShell blocks /healthz) | #1876 is the candidate fix, review-ready | +| #2106 (`code_mode_only` opt-out) | #2112 is a fresh candidate, draft | + +The pattern worth noticing: **five of these already have a candidate PR open.** +The bottleneck is review throughput, not authorship. That is what phase A is +for, and it is why phase A comes before the split rather than after. + +## Sequencing summary + +``` +A: cheap review-ready merges (no split work, unblocks 6 issues) +B: #2080 + #1934 (+ #2112 if ready) (overlapping PRs, incl. 2 bug fixes) +C1: #2019 value moves -> dev (parent; most runtime exposure) +C2: retarget #2023 to dev, re-verify, merge (type-only; parity audit is its gate, not the soak) +C3: preview + 5-7 day soak <- first real gate +C4: #2036 config leaf -> dev alone (startup-order exposure; freeze around the cut) +C5: preview + soak -> promote to latest +D: WP2b stateful config train (only if C5 is clean) +``` + +Most bug PRs flow continuously alongside A-C. The exceptions are the 10 PRs +that touch `src/types.ts` or `src/config.ts`, two of which are bug fixes; they +either land in phase B or pay a rebase after phase C. + +## What this document got wrong, and why that is worth recording + +An adversarial review of the first draft returned FAIL on four counts, and all +four were real: + +1. **A dependency error.** It proposed merging `#2023` before `#2019` because + `#2023` is safer. `#2023` is a *child* of `#2019` and contains it; the + order was impossible. Risk ranking does not override ancestry. +2. **A wrong count.** 6 overlapping PRs became 10 once every open PR was + enumerated instead of sampled, and 2 "already conflicting" became 4. +3. **A backwards justification.** The dogfooding argument rested on a packaging + failure that `files: ["src"]` makes impossible, and applied it to the one PR + (`#2023`) whose contents are erased at compile time. +4. **An overstated risk.** `#2036`'s TDZ story described a hazard the code does + not have — the cyclic bindings are hoisted function declarations, and the PR + removes the cycle rather than adding one. + +The pattern is the same one 060 recorded from the campaign itself: **the +conclusions mostly survived and the reasons did not.** Isolating `#2036` is +still right, just not for the stated reason. Landing overlapping PRs early is +still right, but for six PRs rather than three. A plan whose reasoning is wrong +in this way still produces roughly correct actions — right up until someone +reuses the reasoning for a decision it does not fit. diff --git a/devlog/_plan/260819_next_roadmap/080_merge_loop_ledger.md b/devlog/_plan/260819_next_roadmap/080_merge_loop_ledger.md new file mode 100644 index 0000000000..f8785f69a5 --- /dev/null +++ b/devlog/_plan/260819_next_roadmap/080_merge_loop_ledger.md @@ -0,0 +1,550 @@ +# 080 — Merge-loop ledger + +Append-only record for the batched merge loop. One section per work-phase. + +Loop: HOTL, session `01a01949`, goalplan +`merge-the-reviewed-opencodex-pr-queue-in-small-v`. + +## Standing rules + +- **Small batches.** 2-4 related PRs per work-phase, never a whole stage at once. +- **Fresh review before every merge.** A verdict from an earlier session is not a + merge authorization if the head moved. +- **HOLD list never merges:** #2100, #2077, #2056, #2062, #2063. +- **No contributor branch is ever rewritten.** Defects on a fork head are + requested, not pushed. + +### Head-drift check (added after wp1) + +Record the head SHA a verdict was issued against, and re-check it before the +merge. wp1 proved why: #2102's head moved from the reviewed commit to +`914ee9372`, the author had changed the very code the verdict was about, and +merging on the stale verdict would have shipped a regression the earlier review +could not have seen. + +Heads at wp2 planning time: + +| PR | Head now | Verdict issued against | +|---|---|---| +| #2085 | `eceaf0b6e` | earlier session (head has since moved) | +| #2086 | `f40891410` | earlier session (head has since moved) | + +Both moved. Both get a fresh lane before merging, same as #2102 did. + +## wp1 — #2102 re-reviewed and deferred + +Outcome: **partially blocked on the author — merge deferred, not abandoned.** + +### What changed since the earlier verdict + +The earlier session posted a merge recommendation for #2102 with one request: its +sanitizer sat outside the `if (forward)` branch and so also stripped +`prompt_cache_retention` from API-key passthroughs. The author pushed +`914ee9372` ("preserve key-auth cache retention") in response. + +**Re-reviewing on the new head was the right call and it caught a second defect.** +Merging on the stale verdict would have shipped it. + +### The remaining defect + +`forward` is `provider.authMode === "forward"` alone +(`src/adapters/openai-responses.ts:1483`). That is not "the ChatGPT backend" — +this repo supports noncanonical forward providers, exercised at +`tests/openai-responses-passthrough.test.ts:19-61`. + +| Provider | GPT-5.6 request | +|---|---| +| canonical ChatGPT forward | stripped — correct (#2092) | +| custom endpoint, `authMode: "forward"` | **stripped — regression** | +| API-key / custom endpoint | preserved — fixed by `914ee9372` | +| non-GPT-5.6 model | preserved | + +The fix is one call: gate on `isCanonicalOpenAiForwardProvider(provider)` +(`src/providers/openai-tiers.ts:33`, already used in five places) instead of +`forward`. + +**This file already makes the identical argument 30 lines below the new code**, on +the routed-compaction gate: "an authMode check would let a noncanonical custom +forward provider skip this rewrite while the server still routes it as a +summarizer turn (#422)". The same trap, caught once before, re-entered in a new +function. + +### Action taken + +Requested on the PR ([5341457142](https://github.com/lidge-jun/opencodex/pull/2102#issuecomment-5341457142)) +with the in-file precedent quoted, rather than pushing to +`lilinxiong/fix/gpt56-prompt-cache-retention` — it is a fork head. + +`#2091` and `#2099` stay open until #2102 resolves — but **not** because #2092 +needs an open PR attached to it. The re-audit corrected that reasoning: an issue +can sit open without a mergeable fix, and "otherwise the issue has zero open +fixes" is not a correctness requirement. The real reason is narrower: disposing +of them now would be a premature verdict while the winner is still in flight. + +Neither is a viable fallback if #2102 stalls. #2091 strips from every forward +request including GPT-5.5 and custom-forward providers. #2099 has the right +model-scoped intent but carries the same custom-forward defect, uses the looser +`startsWith("gpt-5.6")` predicate, and includes an unrelated `package.json` +version change. If #2102 stalls, #2092 stays open. + +### One collision to watch + +`#2040` also changes `src/adapters/openai-responses.ts` and +`tests/openai-responses-passthrough.test.ts`. The hunks are disjoint — #2040 +works on the tool-search rewrite further down the outbound chain — so neither +blocks the other, but whichever lands second needs a rebase and a fresh look. + +Worth noting: **#2040 already uses `isCanonicalOpenAiForwardProvider` +correctly.** Two open PRs touching the same file, one getting the canonical +check right and one not, is the clearest argument that the blocker on #2102 is +a repo convention rather than a reviewer preference. + +### Not folded into the author request + +Provider-qualified ids (`openai/gpt-5.6-sol`) are decoded to the bare native id +by the router (`src/router.ts:611-634`, pinned at +`tests/codex-routing.test.ts:304-309`), so the adapter only ever sees bare +GPT-5.6 ids. Asking the sanitizer to recognize the qualified form would +duplicate routing normalization. Out of scope, deliberately. + +## wp2 — #2085 + #2086 merged + +Outcome: **DONE.** + +| PR | Head reviewed | Merge commit | In `origin/dev` | +|---|---|---|---| +| #2085 admission window | `eceaf0b6e` | `e0585e59e` | yes | +| #2086 `ocx models` CLI | `f40891410` | `32d7b7939` | yes | + +Both heads had moved since the earlier verdict, so the head-drift rule applied +and a fresh lane (`01a019c8`) reviewed the current code. It returned MERGE for +both, and it did the thing that makes a review verdict worth acting on: it ran +the new tests against the **unfixed** production code. + +| PR | Against unfixed code | On the merged head | +|---|---|---| +| #2085 | 19 pass, **3 fail** | 22 pass, 0 fail + typecheck | +| #2086 | 16 pass, **2 fail** | 18 pass, 0 fail + typecheck | + +That is a real oracle, not an assertion that the tests exist. + +### What the drift check found this time + +Nothing harmful — but #2086's moved head is not the diff the earlier verdict +covered. It now orders `noVisionModels` **before** `modelInputModalities` +(`src/cli/models.ts:108-109`), matching `isModelTextOnly` +(`src/vision/index.ts:33-35`), which returns on the no-vision match before it +reads modalities. That is behavior beyond a lookup migration, and it is the +correct addition: without it the CLI advertises image support the proxy then +rejects. + +Two work-phases, two moved heads, two materially different diffs. The rule is +earning its cost. + +### Recorded weakness + +`tests/cli-models.test.ts:239-262` (exact-over-family) is **wholly vacuous** — +it passes before the fix. Merged anyway because the other two cases in that file +are genuine oracles, but it should not be cited as coverage. + +### Guard held + +`#2100` and `#2077` — the two HOLD verdicts from the same `modelRecordValue` +family — are still OPEN and unmerged. Merging the batch did not sweep them in. + +### Batch composition check for wp3 + +Recorded before the next cycle so the batch is chosen on evidence rather than +on the roadmap's guess: + +| PR | Owner | Files | Overlap risk | +|---|---|---|---| +| #2035 | iF2007 | `providers/antigravity-models.ts` + test | none | +| #2031 | lidge-jun | `providers/registry.ts`, `structure/03`, 2 tests | registry is a split-program target later, not now | +| #1878 | lidge-jun | one docs-site page | none | + +Disjoint. Safe as one batch of three. + +Note `#2031` touches `src/providers/registry.ts`, which WP3 of the split +program will eventually rewrite — but that work package is not scheduled in +this loop, so there is no ordering constraint today. Worth carrying forward if +the registry split is ever queued. + +## wp3 — #2035, #1878 merged; #2031 rebased + +Outcome: **DONE — all three merged.** + +| PR | Merge commit | Note | +|---|---|---| +| #2035 Google reasoning tiers | `35664ad2e` | merged directly | +| #1878 tool-search docs | `a97c70d4e` | merged directly | +| #2031 MiMo vision sidecar | `7a2d13a74` | rebased first, then merged on green CI | + +### The lane's verdict was right about the code and wrong about the blocker + +It returned DO-NOT-MERGE on all three, but for governance reasons — "required +CI has not run", "CHANGES_REQUESTED against an older SHA", "no current +maintainer approval". Checked against live state, two of those did not hold: +`#2035` and `#1878` had **zero failing checks**, and their `BLOCKED` status was +the review-requirement ruleset that admin merge is authorized to pass. They +merged. + +The lane's code analysis is what earned its keep, and it was thorough: + +- **#2035** — verified no selectable tier disappears (the "collapse" in the + title was pre-existing behavior; this PR repairs routing *after* collapse). + Oracle: 52/0 fixed vs **50 pass 2 fail** unfixed. +- **#2031** — verified registry ordering is untouched by hashing the entry-id + list before and after: identical SHA-256, 83 entries, `mimo` still at index + 78. That is the exact risk a registry diff carries, checked properly. + Oracle: 50/0 fixed vs **48 pass 2 fail** unfixed. +- **#1878** — verified the documented behavior against current `dev` + (`parser.ts:212`, `bridge.ts:639`, `parser.ts:612`) rather than just + confirming it is docs-only. A doc describing behavior the code lacks is + worse than no doc. + +### #2031 was stale-base, and this time it was proven before merging + +Its CI was genuinely red — 7 failing legs including all four test shards. The +lane called it stale-base. Rather than take that on trust: + +``` +git rev-list --count pr2031..origin/dev -> 60 +rebase onto origin/dev -> clean, zero conflicts +bun test (both touched suites) -> 50 pass, 0 fail +bun x tsc --noEmit -> exit 0 +``` + +Rebased and force-pushed (`dc0334eda` -> `d86a2faed`; it is a branch in our own +repo, not a fork). Cross-platform CI run `32249600228` on the new head: +**completed/success, zero failed jobs** — seven red legs became zero with no +source change other than the rebase. Merged as `7a2d13a74`. + +This is the third stale-base case in this campaign. The pattern is stable +enough to name: **a red CI on a PR more than ~50 commits behind `dev` is a +claim about the base, not about the change, until a rebase says otherwise.** + +Worth stating the converse too, because it is the part that keeps this honest: +the rebase does not *prove* the change is good, it removes the base as an +explanation. #2031 was mergeable because the lane had already verified the code +— registry ordering unchanged by hash, oracle red-driven — and the rebase only +cleared the noise hiding that. + +## wp4 — #2103 merged; #2105 and #2053 held + +Outcome: **batch split 1/3.** This is the first work-phase where the batch did +not survive review, and both holds are real. + +| PR | Verdict | Result | +|---|---|---| +| #2103 xAI tool schema | MERGE | `18e072c8d` | +| #2105 Claude shell hook | DO-NOT-MERGE | [5341955684](https://github.com/lidge-jun/opencodex/pull/2105#issuecomment-5341955684) | +| #2053 OAuth superseded commits | DO-NOT-MERGE | [5341955876](https://github.com/lidge-jun/opencodex/pull/2053#issuecomment-5341955876) | + +### #2103 — clean + +Removes only the root `$schema` key before xAI normalization, gated on the +exact `cli-chat-proxy.grok.com` hostname, so the other providers sharing +`openai-chat.ts` are untouched. Oracle: 2/0 fixed, **0 pass 2 fail** reverted — +both tests fail at their first assertion, so nothing in them is decorative. + +### #2105 — a destructive false negative + +`reconcileShellHook(false)` unconditionally removes the hook +(`src/server/system-env.ts:157-180`), and the call sites collapse every failure +into that one boolean (`src/cli/index.ts:368-371`, `:458-459`). But +`injectSystemEnv()` returns false for a custom `ANTHROPIC_BASE_URL`, for +another instance owning the environment, for a swallowed injection failure, and +for "`claude` is not on **this process's** `PATH`". + +That last case is the one that will actually happen: `claudeCodeCliInstalled()` +reads `process.env.PATH` (`:134-149`), and a service-started proxy does not +inherit the interactive login shell's `PATH`. So a user with Claude Code +installed, running `ocx` as a service, gets their working `.zshrc` hook +**deleted**. + +The false-positive direction is harmless — it installs a hook, which is what +the old unconditional behavior did anyway. The asymmetry is the whole finding: +**this change made the safe direction conditional and left the destructive one +unconditional.** + +Requested fix: remove only on an explicit "integration disabled" reason, not on +"not true". + +### #2053 — the code is right and the test is missing + +I asked the lane to hunt for a TOCTOU window on this one because it is an auth +boundary. There is none: the ownership check runs under the file lock with no +`await` before the synchronous write (`src/oauth/store.ts:468-475`, write at +`:185-195`). + +The blocker is elsewhere. Reauthentication is wired through +`assertBeforePersist`, but **removing only that wiring leaves every suite green +— 24 pass, 0 fail.** So a later refactor can delete the reauth protection +silently while a canceled account's credential gets overwritten and +`needsReauth` cleared (`src/oauth/store.ts:644-649`). + +Worth naming the shape, because it recurs: *the fix is correct, the test proves +a neighbouring fact.* The superseded-**login** test carries its oracle only in +its final assertion — the first two pass against the unfixed code +(`tests/oauth-public-surface.test.ts:496-503`). + +### What wp4 changed about the loop + +Three work-phases merged everything reviewed. This one merged a third. That is +the batching rule doing its job: had these been merged as one stage-1 sweep, +two defects would have landed behind a green CI, and the shell-hook one deletes +user configuration. + +### Composition check for wp5 and wp6 + +| wp | PRs | Files | Note | +|---|---|---|---| +| wp5 | #1876 | Windows catalog discovery | closes issue #1852 | +| wp6 | #2112, #1934, #2080 | `types.ts` / `config.ts` overlap set | must land before the split rewrites those files | + +wp6 is the one with a deadline attached: those three are the PRs the split +would otherwise force back onto their authors. #2112 and #1934 are bug fixes, +so the cost of leaving them is paid by users, not just by the queue. + +## wp5 — #1876 fixed, then merged + +Outcome: **DONE**, merge commit `c035ee093` (closes issue #1852). + +This is the phase where the review lane found something worth the whole loop. + +### The blocker: a fix that traded a hang for a wrong answer + +#1876 moves Windows app-server enumeration off the request path so a slow +PowerShell CIM walk stops blocking `/healthz`. Correct goal. But a catalog write +can invalidate the cache while that enumeration is still running, and the code +only suppressed the **cache write**: + +```ts +if (requestCatalogStateGeneration === generation && requestCatalogStateFlight === flight) { + requestCatalogStateCache = { ... }; // correctly skipped after invalidation +} +return status; // but the caller still got the pre-write status +``` + +The awaiting v2 request therefore received `fresh` — and `fresh` is the single +state that authorizes positive model guidance +(`src/server/responses/collaboration.ts:279-280` returns null for +`stale`/`unknown`). So the request would advertise the newly written disk +catalog to an app-server whose in-memory copy that same write had just made +stale. + +The lane reproduced it deterministically rather than describing it: + +```json +{"observed":"fresh","observedCatalogMtime":1000,"actualPostWriteRelation":"stale because 2000 <= 3000"} +``` + +**A slow answer was the bug. A wrong answer is worse than the bug.** + +### Fixed on our branch + +An invalidated observation now returns `unknown` — which is what it actually +knows, and which the guidance path already treats as "say nothing positive". + +The existing regression had asserted `state: "fresh"` for exactly this case, so +**the test was pinning the defect**. It now asserts `unknown`, plus a companion +proving the next post-write observation still reports `fresh` rather than being +poisoned by the degrade. Both fail when the fix is reverted. + +### Then the oracles themselves got audited + +Three follow-up commits, each earned: + +| Commit | What it fixed | +|---|---| +| `4ff8456e4` | the async test injected an **already-async seam**, so it stayed green when the production default was reverted to `execFileSync` — it described the design without guarding it | +| `d55bc920d` | only one of three async wirings was guarded; the other two could be reverted silently | +| `ca7923a59` | the fixture was a POSIX `.sh` (unrunnable on the platform this fix is *for*), and the assertion counted `setInterval` ticks against a hardcoded midpoint — a loaded runner could fail a correct implementation | + +That last one is the sharpest lesson in this loop so far: a test can be a real +red-green oracle **and still be wrong**, if what it measures is machine speed. +Replaced with a phase signal — did any event-loop work run while the child was +alive — which a synchronous exec cannot produce regardless of hardware. + +### Stale base, fourth occurrence + +67 commits behind, 7 red legs, clean rebase, 96 pass 0 fail, `tsc` exit 0. +Final CI on `ca7923a59`: **completed/success, zero failures.** + +One operational note: three intermediate runs reported `ci failure` while every +individual job passed. The cause each time was `platform-macos=cancelled` from +concurrency supersession — a new head cancelling the previous run. The gate job +treats `cancelled` as not-passed, correctly. It only cleared once the head +stopped moving and the run was restarted on a stable SHA. + +## wp6 — all three merged, two after we fixed them + +Outcome: **3 of 3 merged.** + +| PR | Merge commit | How it landed | +|---|---|---| +| #2112 code_mode_only opt-out | `dbe260131` | clean verdict, merged as-is | +| #1934 namespaced tool aliases | `a5289aad5` | blocker fixed by us, then merged | +| #2080 OpenRouter FastWire B2 | `4edf7954f` | blocker fixed by us, then merged | + +The review lane returned MERGE / DO-NOT-MERGE / DO-NOT-MERGE and called both +blockers "small and mechanical". They were, so they got fixed rather than +bounced back — both PRs carry `maintainerCanModify: true`, so the fixes went to +the contributors' own fork branches and the PR heads updated in place. + +### #1934 — the alias mapping was one-way + +The bridge emits a client-facing custom call carrying only the bare name — +`{"type":"custom_tool_call","name":"exec"}` even for a tool declared as +`mcp__functions__exec` (`src/bridge.ts:1031`). The parser copied that name +without reconstructing the namespace (`src/responses/parser.ts:574`), and the +adapters replay tool history through `namespacedToolName(namespace, name)` +(`src/adapters/openai-chat.ts:719`). So the replayed call targeted a bare +`exec` the provider may not expose. + +The lane reproduced it rather than describing it: + +```json +{"responseItem":{"type":"custom_tool_call","name":"exec"}, + "replayedCall":{"name":"exec","customWireName":"exec"}} +``` + +Fixed by rebuilding the namespace from the request's own tool catalog at parse +time. `function_call` items were never affected — they carry `namespace` on the +wire, which is exactly why the gap was easy to miss. + +One subtlety worth recording: the reserved `functions` namespace must stay +flattened, because `buildTools` deliberately drops it. Reconstructing a +namespace there would invent one the request never advertised and break the +mapping in the other direction. Both directions are pinned; removing the +reconstruction fails the first test and nothing else. + +Pushed as `135872d25` to `jenfonro/opencodex`. + +### #2080 — a definite price for an outcome nobody observed + +An assumed Fast attempt reported the standard total with no uncertainty marker +(`src/usage/cost.ts:367`, `:438`, `:457`, `:478`). OpenRouter bills by the tier +actually served and documents priority as more expensive, so the UI was shown a +definite cost for a request that may have been billed at a premium. + +The confirmed case was already treated as a lower bound, because the premium +endpoint price is not bundled here. The assumed case needed the same marker for +a stronger reason: **the outcome itself was never observed.** Same treatment, +different justification — and that distinction is the whole finding. + +Route pinning turned out to be a non-issue: the adapter writes `service_tier` +independently and preserves any existing `provider.order`, `provider.only`, and +`allow_fallbacks` (`src/adapters/openai-chat.ts:1308`). + +Pushed as `e1ef7942b` to `olddonkey/opencodex`. + +### The overlap debt is paid + +wp6 existed to land the PRs touching `src/types.ts` and `src/config.ts` before +the split rewrites those files. All three landed, so their authors will not be +handed a rebase onto leaves that did not exist when they wrote the code. + +Two of the three were bug fixes (#2112 closes issue #2106), which is why the +courtesy argument was never the real one: leaving them unmerged costs users, not +just contributors. + +### Split-stack state entering wp7 + +This loop's own merges moved `dev` well ahead of the split branches. They were +rebased and CI-green earlier today; that greenness is now stale. + +| PR | Base | Head | Behind dev | Blocking checks | +|---|---|---|---|---| +| #2019 | `dev` | `a2eb3c30c` | 13 | hygiene, enforce-target | +| #2023 | `codex/split-wp1-types` | `874598bd3` | 42 | hygiene, enforce-target | +| #2036 | `dev` | `6c6925a4d` | 42 | hygiene, enforce-target | + +#2019 is only 13 behind because its head already moved once during this loop; +the other two carry the full drift. + +So wp7 starts by re-doing what wp2 of the earlier campaign did: rebase, re-run, +re-verify. That is not wasted work — it is the cost of a stack sitting behind an +active queue, and it is exactly the cost the roadmap said would compound if the +split kept waiting. + +`hygiene` and `enforce-target` are the same two gates as before. `hygiene` is +`missing_regression_test`, which is correct for a pure-move PR and resolves with +`test-exception-approved` (`.github/scripts/pr-hygiene.cjs:153`). Note the label +is stripped on every new head (`pr-hygiene.yml:102`), so it must be applied +**after** the final rebase push, not before. + +### Verified clean on this head + +- The key-auth test is a real oracle: it fails against `72117f169`. +- The model predicate is correctly delimited — `gpt-5.60` cannot match, unlike a + raw `startsWith("gpt-5.6")`. +- No scope creep; no ordering conflict with `stripUnsupportedForwardParams` + (disjoint keys). +## wp7-wp9 — the split stack landed, and one blocker survived + +Outcome: **DONE**, bottom-up and in order. + +| PR | Merge commit | +|---|---| +| #2019 WP1 value helpers | `da86a830a` | +| #2023 WP1b type-only barrel | `2235f456d` | +| #2036 WP2a config leaf | `eca18d0c8` | + +All three are ancestors of `origin/dev` in dependency order — parent before +child before the independent leaf, exactly as ancestry required. + +### A blocker that survived its own hold + +wp6 held #2112 for a specific reason: `codexToolMode` existed only in the +TypeScript interfaces, never in `providerConfigSchema`, and that schema ends in +`.passthrough()`. It merged anyway (`dbe260131`) with the other two overlap PRs, +which did get follow-up fixes (`135872d25` for the #1934 namespace leak, +`e1ef7942b` for the #2080 assumed-priority cost). This one did not. + +Verified on the landed tree rather than assumed: `grep -c 'codexToolMode' +src/config.ts` returned **0**, while `apiKeyTransport`, `upstreamHttpVersion`, +and `codexAccountMode` are all validated enums in that same schema. + +Fixed on `dev` as `d697e2553`: `codexToolMode` is now a declared +`z.enum(["code_mode_only", "shell"])`, with a regression that drives red when +the enum line is deleted (152 pass / 1 fail) and green with it (153 / 0). + +**The lesson is about hold hygiene, not about this field.** Three PRs were held +with three blockers; two were fixed and one was not, and the merge did not +distinguish between them. A hold is only worth what the re-check before merge is +worth — and `.passthrough()` is exactly the kind of defect that leaves no trace +at the merge boundary, because nothing fails. + +### The exception argument was wrong, and the review proved it + +A lane reviewing `#2019` was asked to judge whether the +`test-exception-approved` argument actually held. Its answer: **no**, and it +named the cheap test that would have caught the plausible mistake. + +The exception said a pure move's oracle is `tsc` plus the ~400 files importing +through the barrel. That is true for the **types** — they are erased, so a wrong +one fails compilation. It is false for the **runtime values**, and nobody had +separated the two cases. + +No test imported `src/types/tools.ts` or `src/types/wire.ts` directly. Every +import went through the barrel, so barrel and leaf were never compared to each +other. A barrel that re-declared a value instead of re-exporting it would pass +every suite in the repository. + +Demonstrated rather than argued: forking `MODEL_ADAPTER_OVERRIDE_ALLOWED` into a +second `Set` inside the barrel leaves `tsc --noEmit` at **exit 0**. Two `Set` +instances where the code assumes one — the singleton-forking hazard the original +split risk assessment listed as a MEDIUM program risk and left to *review greps*. + +Added as `8f0c1e674`: `tests/types-barrel-identity.test.ts` asserts reference +identity between barrel and leaf for all 15 moved runtime values, plus a +both-directions reachability check. An ESM re-export binds the same object, so +`toBe` passes for a genuine re-export and fails for a copy, a wrapper, or a +re-declaration. Driven red: the forked `Set` fails exactly two assertions. + +**"No test is possible" was a claim, not a fact.** It survived three campaigns +of this document asserting it — including one where I wrote that a barrel test +"restates the compiler". It does not. It states something the compiler cannot +see. diff --git a/src/config.ts b/src/config.ts index 9f590e7323..60178f3d4f 100644 --- a/src/config.ts +++ b/src/config.ts @@ -728,6 +728,11 @@ const providerConfigSchema = z.object({ .optional(), retryOn429: retryOn429PolicySchema.optional(), codexAccountMode: z.enum(["pool", "direct"]).optional(), + // Validated rather than passed through: this schema ends in `.passthrough()`, so an + // undeclared key survives verbatim. A misspelled `codexToolMode` therefore used to be + // accepted, persisted, and then silently resolved to the `code_mode_only` default — the + // operator asked for shell mode, got code mode, and was told nothing (#2106). + codexToolMode: z.enum(["code_mode_only", "shell"]).optional(), responsesItemIdRepair: z.object({ message: z.array(z.string().min(1)).optional(), reasoning: z.array(z.string().min(1)).optional(), diff --git a/tests/config.test.ts b/tests/config.test.ts index f05f107a8e..4f86db8743 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -801,6 +801,34 @@ describe("opencodex config defaults", () => { } }); + test("accepts both codexToolMode values and rejects a misspelled one (#2106)", () => { + for (const codexToolMode of ["code_mode_only", "shell"] as const) { + writeConfig({ + port: 12345, + providers: { + custom: { adapter: "openai-chat", baseUrl: "https://example.test/v1", codexToolMode }, + }, + defaultProvider: "custom", + }); + expect(readConfigDiagnostics().config.providers.custom.codexToolMode).toBe(codexToolMode); + expect(readConfigDiagnostics().error).toBeNull(); + } + + // The regression this guards: `providerConfigSchema` ends in `.passthrough()`, so an + // undeclared key survives verbatim. Before the enum was declared, "shel" was accepted, + // persisted, and then silently resolved to the `code_mode_only` default — the operator + // asked for shell mode, got code mode, and was told nothing. + writeConfig({ + port: 12345, + providers: { + custom: { adapter: "openai-chat", baseUrl: "https://example.test/v1", codexToolMode: "shel" }, + }, + defaultProvider: "custom", + }); + expect(readConfigDiagnostics().source).toBe("fallback"); + expect(readConfigDiagnostics().error).toContain("codexToolMode"); + }); + test("accepts the exact responsesItemIdRepair shape and rejects the old nested placeholderIds proposal", () => { writeConfig({ port: 12345, diff --git a/tests/types-barrel-identity.test.ts b/tests/types-barrel-identity.test.ts new file mode 100644 index 0000000000..7508d45402 --- /dev/null +++ b/tests/types-barrel-identity.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, test } from "bun:test"; +import * as barrel from "../src/types"; +import * as tools from "../src/types/tools"; +import * as wire from "../src/types/wire"; + +/** + * The mega-file split turned `src/types.ts` into a re-export barrel. Its correctness + * argument was "tsc plus the ~400 files that import through it", which is true for + * TYPES — they are erased, so a wrong one fails compilation. + * + * It is not true for the runtime values. A barrel that copied an implementation, wrapped + * one, or re-declared a `Set` would typecheck cleanly and pass every existing suite, + * because no test imported a leaf directly — every import went through the barrel, so + * barrel and leaf were never compared to each other. + * + * A forked `MODEL_ADAPTER_OVERRIDE_ALLOWED` is the concrete hazard: two `Set` instances + * where the code assumes one, diverging the moment anything mutates or identity-checks it. + * The original split risk assessment named singleton forking as a MEDIUM program risk and + * relied on review greps to catch it. This is that check, mechanised. + * + * Reference identity is the right assertion: an ESM re-export binds the same object, so + * `toBe` passes for a genuine re-export and fails for a copy, a wrapper, or a re-declaration. + */ +describe("types barrel re-exports the leaves by identity, not by copy", () => { + test.each([ + "namespacedToolName", + "toolChoiceAliases", + "toolChoiceCandidates", + "toolAllowedByChoice", + "resolveToolChoiceWireName", + "modelInList", + "isAllowedToolChoice", + "toolChoiceToolPredicate", + ] as const)("types/tools %s is the same binding", name => { + expect(barrel[name]).toBe(tools[name]); + }); + + test.each([ + "UPSTREAM_HTTP_VERSION_VALUES", + "REASONING_SUMMARY_DELIVERY_VALUES", + "OPENAI_PROVIDER_TIER_VERSION", + "MODEL_ADAPTER_OVERRIDE_ALLOWED", + "captureWireAdapterHardPins", + "isWirePinnedModel", + "pinnedWireAdapter", + ] as const)("types/wire %s is the same binding", name => { + expect(barrel[name]).toBe(wire[name]); + }); + + test("the wire allowlist is one Set, not two", () => { + // Called out explicitly because it is the only mutable-shaped value in the moved set, + // and a fork here is invisible to every other assertion in the repository. + expect(barrel.MODEL_ADAPTER_OVERRIDE_ALLOWED).toBe(wire.MODEL_ADAPTER_OVERRIDE_ALLOWED); + expect([...barrel.MODEL_ADAPTER_OVERRIDE_ALLOWED]).toEqual([...wire.MODEL_ADAPTER_OVERRIDE_ALLOWED]); + }); + + test("every runtime value the leaves export is reachable from the barrel", () => { + // Guards the other direction: a leaf can grow a new export that the barrel forgets to + // re-export, which no consumer notices until one tries to import it from the barrel. + for (const [leafName, leaf] of [["tools", tools], ["wire", wire]] as const) { + for (const [name, value] of Object.entries(leaf)) { + if (typeof value !== "function" && typeof value !== "object" && typeof value !== "number") continue; + expect({ leaf: leafName, name, present: name in barrel }).toEqual({ leaf: leafName, name, present: true }); + } + } + }); +});