Skip to content

Merge stable OpenCodex v2.100.1 - #14

Merged
bcdonadio merged 126 commits into
customfrom
update/upstream-2.100.1
Nov 10, 2026
Merged

bcdonadio merged 126 commits into
customfrom
update/upstream-2.100.1

Conversation

@bcdonadio

@bcdonadio bcdonadio commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Merge stable upstream v2.100.1 (00c02163e4352be5beb5633a3b6cc3b0be94ec8a) into custom, preserving upstream ancestry, seven-day usage retention, transaction diagnostics, Hub entitlement versioning, and the signed numbered fork-release workflow.
  • Reconcile request dispatch, header forwarding, byte-offset ledger indexing, and fork-specific trusted CI branch selection. Adopt upstream's newer Fable-compatible Claude identity and single-dev upstream policy while retaining this fork's custom target.
  • Synchronize all four version sources to 2.100.1: upstream's tag retains 2.100.0 source metadata, which cannot satisfy the fork's signed package/tag binding. Preserve existing exact dependency pins. Split generated telemetry documentation into its own chapter to keep the existing size guard.

Start review with runtime conflict resolutions and ledger compatibility, then the trusted CI branch selection and version-source correction. The fork archive now includes the upstream-declared ocx-child source files, with bounded inventory verification; this does not add automatic Rust compilation. The fork's numbered release workflow remains the release path; upstream's repository-specific release workflow is not used here.

Verification

  • Pinned Bun 1.4.4 and frozen lockfiles. Focused validation: 75 initial runtime tests; 253 integration cases plus the corrected diagnostics file (76/76); 102 retention/entitlement/layout cases; 239 scanner/filter/history/CI cases; 16 GUI cases; CJS policy tests; 10 version-source tests and 19 timeline/retention acceptance tests. All pass after the documented fixture adjustment. Version-source tests run with fixture-only Git signing disabled because this host inherits mandatory signing into temporary repositories.
  • Post-merge git diff --check is clean; upstream-owned whitespace was preserved unchanged. Merge signature and both parent identities were verified.
  • One diagnostics fixture now expects the upstream-preserved User-Agent while retaining its sanitized-diagnostics assertion. The compacted timeline/accounting and seven-day raw-detail cutoff remain intact.
  • Explicit semantic/security review covered auth/header handling, privacy-controlled labels, dispatch ownership, and CI checkout trust. Existing pinning is retained; Socket checked the newly introduced documentation fonts and the reused Bun version. Font findings concern font licensing/popularity; no reported vulnerability.
  • The invoked update skill selects focused tests, required hosted checks, signed release verification, and live acceptance. Full suite, standalone typecheck/lint/privacy, and local GUI build are omitted under that workflow; no unrun check is described as passing.
  • Preflight: ready local client, fresh installed Codex app-server with 29 visible models and ChatGPT authentication, and completed authenticated WebSocket inference through the existing relay. Private operational evidence is excluded.

Upstream GUI evidence from upstream PR #8471, using synthetic data; this is upstream evidence, not a new local rendering:

Upstream telemetry consent pending state

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

Hosted policy readback: custom requires PR integration but zero approvals and no status contexts. DCO reports inherited upstream commits without sign-offs; those commits are preserved. Cross-platform CI and PR target/hygiene workflows were already disabled and remain unchanged.

github-actions Bot and others added 30 commits November 8, 2026 07:18
…n#8313)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(vertex): support dashboard ADC configuration

* fix(vertex): fence ambient credentials at dispatch

* fix(vertex): address credential and setup review findings

* fix(vertex): address final credential review

* fix(vertex): address routing and discovery review

* fix(vertex): preserve registry and catalog authority

* fix(gui): satisfy Vertex form accessibility checks
…nder Advanced usage (lidge-jun#8006)

* docs(readme): link GOAT up top, note official-API commercial use, move account pool under Advanced usage

- Add a one-line 'Part of GOAT' link under the tagline.
- Add a commercial-use note: GOAT's paid offerings use official provider APIs and agreements only.
- Move the ChatGPT account pool section and its highlight bullet (with the provider-policy note intact) into a new Advanced usage section before Documentation. No content removed.
- Resync all seven locale READMEs and refresh readme/i18n-manifest.json hashes.

* docs: remove unverified commercial-use claim
* docs: plan automatic update parity

* docs: clarify update roadmap and address review feedback

* docs: pin update guard citations to inventory baseline

* docs: align update guard citations with baseline
* fix(gui): show WorkBuddy credits in quota summary

* fix(gui): represent unavailable WorkBuddy credits accurately

* fix(gui): handle accumulated WorkBuddy credit precision

* fix(gui): define WorkBuddy credit percentage rounding

* fix(gui): omit inconsistent WorkBuddy credit ratios

* fix(gui): round reported WorkBuddy balances precisely

* fix(workbuddy): preserve decimal precision in credit sums

* fix(workbuddy): preserve enterprise balance precision

* fix(workbuddy): preserve exact quota balance decimals

* fix(workbuddy): validate exact balances before rounding

* fix(workbuddy): sum credit amounts before number conversion

* fix(workbuddy): keep quota change within display scope

Defer exact-decimal DTO preservation to lidge-jun#8324.

Co-authored-by: Rafael Moreira Lima <rrmlima@gmail.com>

---------

Co-authored-by: Rafael Moreira Lima <rrmlima@gmail.com>
… on Anthropic children (lidge-jun#7374)

Preserve native Anthropic Messages requests across eligible failover combos, including replay commitment, accounting, and stream framing.

Co-authored-by: Vadym O <bolein95@gmail.com>
* refactor(release): build channels from verified dev commits

* fix(release): preserve recovery and bound evidence lookups

* fix(ci): fetch release history for evidence tests
…n#8334)

* fix(gui): show verified Claude account in overview

* fix(gui): order account status refreshes by provider

* fix(gui): keep account identity uncertain during logout

* Fix OAuth status races across logout and targets

* Fix OAuth account mutation status races

* Fix OAuth account operation races and recovery

* Fix OAuth cancellation account ownership

* Fix account login cancellation and logout coordination

* fix(gui): narrow OAuth login coordination

* fix(gui): preserve explicit account removal failure path
* test(codex): diagnose native claim setup failures

* fix(tests): preserve claim diagnostics and error behavior
Accept a single leading BOM in JSONC configuration while rejecting repeated BOMs before a rewrite.

Co-authored-by: 정우철 <oocheol@naver.com>
Count supplementary Han consistently in shared and Kiro token estimates, including mixed-script input.

Co-authored-by: 정우철 <oocheol@naver.com>
Stop processing sidecar frames after the first terminal event, regardless of network chunk boundaries.

Co-authored-by: 정우철 <oocheol@naver.com>
Prefer valid titles from completed sidecar citations while retaining safe streamed fallbacks.

Co-authored-by: 정우철 <oocheol@naver.com>
…compare Lab file identity with bigint stat (lidge-jun#8035)

* fix: compare Lab file identity with bigint stat

NTFS file IDs can exceed 2^53, where adjacent IDs are equal as JS numbers. Take dev/ino from bigint stats in the Lab mutation lock, private-file stage cleanup, private reads, artifact and scratch directory checks.

* test: select Lab injected stat targets by canonical path

* fix: bind Lab lock release to the checked directory

Release, stale reclaim and failed-acquisition cleanup move the canonical lock to a unique detached name first and remove it only when the moved directory has the checked identity and the caller's owner or reclaim token; any other directory is restored or left detached.

* fix: release the Lab lock without moving a directory it does not own

Release and failed-acquisition cleanup move only their own owner record and remove the lock directory only while empty; detached cleanup skips a live owner; an acquisition whose fresh directory vanishes retries.

* fix: serialize Lab public-evidence mutation with an OS-held SQLite lock

Replace the directory lock and its stale-owner reclaim with a BEGIN IMMEDIATE transaction on lab/public-evidence-mutation.sqlite, released by the OS when the handle closes. Older builds' live directory locks are still honoured and never moved or removed.

* fix: bind the Lab lock file to the inspected file and keep live detached locks

Create the lock database exclusively with mode 0600, re-prove its identity after locking, and skip older builds' detached copies that still record a live owner.

* fix: skip Lab lock bookkeeping names in community cache scans

* fix(lab): preserve distinct recovery stages

---------

Co-authored-by: JUN <243035832+lidge-jun@users.noreply.github.com>
…lidge-jun#8152)

A failover combo whose native Codex target answered a zero-output 503 server_is_overloaded after Codex WebSocket metadata frames (codex.rate_limits / codex.response.metadata) never moved to its next target: the combo stream preflight treated those frames as output and committed the child. They are now non-committing control frames, so the overload fails over and the declined child's metadata never reaches the client.

Closes lidge-jun#8107

Co-authored-by: Ingwannu <ingwannu@users.noreply.github.com>
* ci: add targeted Windows native claim diagnostic lane

* fix: retain Windows claim diagnostic evidence

* Preserve claim diagnostics across child timeouts

* test: assert timeout without platform exit-code assumptions

* ci: remove unmerged evidence policy fingerprint
…n#8294)

* docs: plan automatic update parity

* Extract offline minisign verification primitives

* Generate proposed update trust root from pinned key

* Define signed update candidate identities

* Add bounded unverified update JSON parser

* Bound aggregate update JSON allocations

* Compare signed update versions exactly

* Verify offline update signature thresholds

* Validate closed signed update document schemas

* Bind generated update root digest into runtime

* Authenticate the embedded update trust root

* Snapshot update root bytes before verification

* Verify signed releases and select bound targets

* Check signed update comment versions

* Add Node update primitive interoperability harness

* Separate root transition and release evidence

* Verify signed update artifact evidence

* Cover artifact delegation and byte mismatches

* Clarify signed root transition envelope contract

* Bind update metadata evidence to signed targets

* Cover signed metadata binding failures

* Verify root transition signer unions

* Exercise transition envelope in runtime harness

* Exercise signed transition evidence across runtimes

* Expose root transition envelope parser

* Allow bounded generated update asset names

* Exercise signed supply chain on Node 18

* Check schema byte caps across runtimes

* Check root expiry across supported runtimes

* Test schema field limits across runtimes

* Exercise malformed Unicode across runtimes

* Prove malformed Unicode rejection is observable

* Bind verifier keys and parser input snapshots

* Exercise full root thresholds on Node 18

* Exercise signed version and legacy adapter parity

* Exercise signed evidence for every target tuple

* Verify evidence across supported channels

* Expose detached signed supply evidence snapshots

* Verify all evidence snapshot bindings

* Add unverified checkpoint payload codec

* Bound checkpoint descriptor capture

* Protect checkpoint encoder collection limits
…e usage dashboard (lidge-jun#8293)

* docs(devlog): plan opt-in telemetry and the opencodex.me usage dashboard

Roadmap unit for a six-layer stack: client telemetry core, GUI consent popup, Cloudflare Worker collector, telemetry guide, and the public /dashboard page. Docs only.

* docs(devlog): record PR review round 1 decisions for the telemetry stack

* docs(devlog): plan the env opt-out marker, privacy statement files, and the open SignPath condition

- 000: the no-backfill rule covers env opt-out days and the disclosure day; the
  privacy statement changes in layer 2.
- 040: lists every file that carries the published privacy statement (README, seven
  translations, both code-signing pages, SPONSORS.md) and says they land in layer 2.
- 060: D2 adds the envOptOutActiveSince marker for a proxy that runs opted out for
  days; records SignPath's install-time condition verbatim as an open owner decision
  instead of assuming first-run disclosure satisfies it.

* docs(devlog): record the owner's SignPath decision (D21) and the Windows installer layer
…-4) (lidge-jun#7495)

* docs(devlog): design Child-to-Home account move over Remote Link (OCX-4)

* docs(devlog): correct OCX-4 link admission facts and tense

Name the existing child-parity report as the third link admission predicate, describe the account-offer route as a fourth, scope validation-pending imports to Orca, and use future tense for routes and features that are not built.
Show Claude CLI subscription quota from the latest recorded account observation, including consistent active and passive reports.

Co-authored-by: Sayo <82053242+wtfsayo@users.noreply.github.com>
…offline journal (lidge-jun#8237)

* docs(devlog): add docs-first roadmap and PRDs for ocx-child ephemeral link

* feat(link): add ephemeral-child contract helpers and golden conformance suite

* feat(ocx-child): add standalone lightweight Rust Child link relay and offline journal

* feat(link): integrate ephemeral ocx-child discovery, enrollment, and sync fan-out

* docs(link): document ephemeral ocx-child in structure SSOT and docs-site

* fix(ocx-child): persist write-ahead journal, track multiline TOML, and restore user features

* fix(link): harden ocx-child Windows ACLs, strict wire/settings bounds, and discovery precedence

* fix(link): preserve interrupted-sync digests, rollback failed reattach, and align locale/conformance parity

* fix(link): fail fast on unreadable attach snapshots, preserve feature comments, and clarify lifecycle docs

* fix(link): verify restored credentials before relay restart and clarify fan-out restart docs

* style(ocx-child): remove trailing whitespace

* fix(ocx-child): preserve recovery and verify shutdown

* fix(ocx-child): harden recovery identity and shutdown

* fix(ocx-child): preserve local state during detach and sync

* test(ocx-child): add env-gated test barrier and shared mock Home harness

Process-level lifecycle regressions need to pause a real ocx-child at a
named point. test_hooks::barrier is a no-op unless both
OCX_CHILD_TEST_BARRIER_DIR and OCX_CHILD_TEST_BARRIERS are set; spawned
serve children never inherit those variables.

The mock Home and binary runners move into tests/common so new test
files can share them; the mock gains parity stall/trickle modes.

* fix(ocx-child): serialize and own the shared CODEX_HOME

The lifecycle lock and journal live under OPENCODEX_HOME/child while
config.toml, the catalog and models_cache.json live in CODEX_HOME. Two
ocx-child installs with different OPENCODEX_HOMEs and one CODEX_HOME took
independent locks and could attach, sync and detach the same files with
incompatible journals.

Every writing lifecycle command now also locks a fixed .ocx-child.lock
inside the canonical CODEX_HOME (never deleted) and checks a durable
ownership record (.ocx-child-owner.json: owner id, canonical
OPENCODEX_HOME, directory identities, phase; no secrets). Lock order is
lifecycle lock, CODEX_HOME lock, daemon start gate. A foreign, corrupt or
unreadable record refuses without mutation and names the recovery step;
ownership is never taken over by PID or timeout. Detach releases
ownership last, after a durable cleanup-complete phase, so any crash is
resumable. Journal application and offline detach take the guard as a
parameter.

Regression: tests/lifecycle_guards.rs runs two real processes against one
CODEX_HOME with a barrier inside the protected section.

* style(ocx-child): format lifecycle assertion message

* fix(ocx-child): recheck home identity around every restoration step

Immediate rollback, crash recovery and offline detach verified the home
directory identity once and then wrote and removed files by path name. If
a home was replaced after that check, restoration kept writing into the
new directory and could delete its recovery records.

Every restoration write, rename and removal now goes through
CodexHomeGuard::restore_step, which re-verifies both captured home
identities immediately before and after the call while the CODEX_HOME
lock is held. A mismatch stops restoration, keeps every recovery record
(rollback snapshot, journal, key, ownership record) and reports a
recovery error. The rollback snapshot and journal are removed only by a
step whose checks passed.

Regression: tests/lifecycle_guards.rs pauses detach between verification
and the write, replaces CODEX_HOME with a directory of sentinel files,
and checks the sentinels, the original directory and the records.

* fix(ocx-child): commit a daemon generation before it serves

spawn_detached_serve started the relay, the parent waited for /healthz
and then recorded the PID; attach removed its rollback record later. If
the parent died after handing over the secret, the child kept serving,
including after recovery restored files or after another generation was
committed.

Each start now records a random daemonGeneration in state before the
spawn and passes it with the secret on stdin, which stays open. Inside
the start gate the child requires state.daemonGeneration to match, and
until it reads "commit <generation>" it answers only /healthz and posts
no parity reports. EOF or any other line before the commit makes it exit
without serving. The parent commits only after the state naming the
generation and PID is durable and, for attach, after the rollback record
is removed. Restart, sync rollover and rollback's prior-relay restart use
the same rule. An operator-run serve --foreground is unchanged and is
refused while an attach rollback is pending.

Regression: tests/lifecycle_guards.rs kills the real attach parent at a
barrier before the commit, and starts a daemon with stale and superseded
generations.

* fix(ocx-child): let shutdown end in-flight parity work and a busy pool

The relay joined its parity thread on shutdown, but an in-flight parity
request had only per-read socket timeouts (5 s/15 s), so a stalled or
trickling Home kept it alive well past stop_owned_daemon's 3-second wait
and detach reported that the daemon did not stop. Separately, the accept
loop blocked on the worker semaphore when all 32 workers were busy, so
the shutdown connection was never accepted.

One-shot Home calls now carry a whole-request deadline (60 s for
lifecycle fetches, 10 s for parity) enforced per read. The parity thread
registers its socket in a CancelSlot; shutdown cancels the slot under the
same mutex and shuts the socket down, and serve exits only after the
thread ends. When the pool is full, a connection gets a short-lived
control thread (capped at 256) that answers only /healthz and the
authenticated shutdown with 1-second timeouts.

Regression: tests/lifecycle_guards.rs runs detach against a Home that
stalls or trickles the parity response and against a relay whose workers
are all blocked, and checks the time budget and that the process is gone.

* test(ocx-child): cover removal of an unchanged injected web_search on detach

* docs(link): state the single-owner CODEX_HOME rule for ocx-child

* fix(ocx-child): let Home catalog slugs win in models_cache sync after outside edits

A sync after any outside edit to models_cache.json rebuilt the cache by
swapping injected Home entries back to their pre-attach versions, then
rejected each restored entry because it differed from the Home catalog
entry with the same slug. Every later sync failed with
client_models_cache_conflict until detach and re-attach. A Codex rewrite
that re-serialized a Home entry hit the same path.

Injected catalog slugs are now Child-owned by slug: sync writes the Home
version and keeps entries with other slugs, and the detach snapshot keeps
the pre-attach version or drops the slug.

* fix(ocx-child): commit a rollback-restarted relay after removing the record

Attach rollback restarted the prior relay and committed it before it
removed the rollback record, so the relay could serve while recovery
might still restore files underneath it. The restart now commits only
after the record is removed; an uncommitted relay is dropped and exits
if the removal fails. The structure doc names this ordering.

* fix(ocx-child): bound each control-mode request head with a deadline

A control connection, accepted while every worker is busy, had only a
1-second per-read timeout, so a sender writing one byte per second held
its slot indefinitely and 256 such senders kept the authenticated
shutdown from being handled. Control mode now reads through the
existing DeadlineStream with a 2-second deadline that covers the whole
exchange, including the shutdown proof frame. The code comment and
structure text now state the bound and the connection cap precisely.

* fix(ocx-child): cap the cancellable parity connect at one second

The parity socket can be registered for cancellation only after it
connects, and that connect could take up to the 5-second auth timeout,
longer than the 3-second wait stop_owned_daemon gives a relay to exit.
A request that carries a CancelSlot now connects with a 1-second bound;
one-shot lifecycle fetches keep the 5-second bound.

* fix(ocx-child): keep both catalogs in the backup across the cache write

sync replaced the catalog backup with the new catalog before it wrote
models_cache.json. An I/O error or crash between the two left a cache of
previous-catalog entries next to a backup naming only the new catalog,
so the next reconcile kept those previous Home entries as local ones and
detach restored them into the native cache.

Before the cache write the backup now names the slugs of both catalogs;
after it, the new catalog alone. The regression interrupts a sync with
JournalWriteFault::AfterCatalogWrite and checks detach, directly and
after a retried sync.

* fix(ocx-child): remove the padding inside the relay restart hint

A source line wrap had put about 20 spaces between "run" and "ocx-child restart" in the message attach prints when its relay does not start.

* fix(ocx-child): compile the test barrier out of release builds

test_hooks::barrier read two environment variables on every lifecycle
path and the env names were linked into the release binary. The barrier,
its env constants, and the env stripping for a spawned relay now exist
only with debug_assertions, the profile cargo test builds the binary
with; a release build compiles them to no-ops.

* fix(ocx-child): open lock files without following links

FileLock checked the path with reject_symlink and then opened it with
create(true), so a symlink swapped in between made the open follow it
and create the link target. The open now uses O_NOFOLLOW on Unix, with
the value libc defines for each target, and FILE_FLAG_OPEN_REPARSE_POINT
on Windows, and refuses anything that is not a plain file. On Windows an
existing lock file is verified to carry the owner-only DACL instead of
being accepted unchecked. A debug-only barrier between the check and the
open lets the regression swap in the symlink.

* fix(ocx-child): wipe the shutdown secret and token-bearing request heads

The shutdown secret was a plain String in ChildStateV1, so every state
read, clone, and drop left a copy in memory and Debug printed it. It is
now a SecretString that is wiped on drop, serialized as the same plain
string, and redacted from Debug; load_live_creds no longer hands out a
copy nobody used, and the rollback records no longer wipe it by hand.

The daemon start line and the relay and lifecycle request heads that
carry the link token are built once at their final size into
SecretBytes, and state.json bytes are wiped after a read or write.

* docs(link): state Child sync serialization and the restore window precisely

The lifecycle reference said a second ocx sync fails fast on any Child.
That holds for a full ocx Child; on ocx-child only the --expect-link form
the Home's fan-out runs exits 5 with pending_operation, and a plain
ocx-child sync waits for the first apply. The structure note now names
who can act in the restore window, and the ocx-child status ledger no
longer carries stale test counts or a stale wp5 summary.

* test(link): find the ocx-child binary under CARGO_TARGET_DIR

The e2e test built ocx-child with cargo but always looked for the binary under native/ocx-child/target, so a run with CARGO_TARGET_DIR set failed with ENOENT. It now resolves the target directory the way cargo does.

* fix(ocx-child): stop printing the link key id after attach

The attach success line named the link key identifier. Keep identifiers out of console output, matching the repository privacy rule; the regression asserts attach output never contains the key id.
…idge-jun#8364)

* fix(gui): show Claude account email when privacy.maskEmails is false (lidge-jun#8286)

Add an accountLabel to the OAuth account roster that follows privacy.maskEmails: the validated masked identity by default, the stored email when the operator sets the flag to false. Dashboard rows, Overview, reset dialogs and remove confirmations prefer it over the always-masked maskedLabel, which tray surfaces keep reading unchanged.

* fix: reject format characters in unmasked account labels and clarify scope (lidge-jun#8286)

Address review notes: omit bidi/zero-width format characters from the unmasked label, cover the unmasked filter and a string "false" config in tests, and document that all OAuth account labels follow privacy.maskEmails while desktop trays stay masked.

---------

Co-authored-by: OpenCodex Lane (cpa) <cpa@localhost>
…ract (lidge-jun#8336) (lidge-jun#8366)

The expected Git Bash PATH entries were rebuilt with the same win32.dirname/win32.join calls as releaseShellPath, so a wrong path operation in the helper could be mirrored by the assertion. Assert literal Windows paths instead.

Co-authored-by: OpenCodex Lane (cpa) <cpa@localhost>
…dge-jun#8330) (lidge-jun#8367)

The release helper ran gh run watch through the captured command runner, so the operator saw only the run ID. Stream the watch's stdout (progress and final job states) and print the run URL; stderr stays captured and unprinted, and a failed run still fails the helper.

Co-authored-by: OpenCodex Lane (cpa) <cpa@localhost>
…idge-jun#8346)

* docs(release): describe history reconciliation and attested resume

structure/ops/docs-and-release.md and structure/ops/cross-platform-ci.md
still said the release workflow refuses resume and fails closed whenever
npm history exists. Describe what lidge-jun#8328 implements instead: channel-floor
history reconciliation, the operator-attested resume with its explicit
version and source inputs, the checks that refuse it, and the rule that a
resume never republishes npm.

Closes lidge-jun#8329

* docs(release): correct legacy floor and resume tag/release checks

For the two fixed 2.99.0 legacy floors, collectPublicationHistory replaces
the containment check with the fixed-SHA, baseline-ancestry and
version-only tree checks in verifyLegacyTreeEvidence. On resume,
release-resume.cjs only checks a tag or release that already exists, and
publish creates whichever is missing. The npm history read is not an
authenticated GitHub request.

* docs(release): name dispatch inputs, align the contributing guide, guard stale release text (lidge-jun#8329)

Maintainer fix-forward: list the release dispatch inputs and note that scripts/release.ts never dispatches a resume, replace the hosted contributing guide's 'reconciliation outstanding' wording, and add a contract test that requires every dispatch input and recovery gate script in the Release workflow section and rejects the stale refusal text in all three docs.

---------

Co-authored-by: lidge-jun <lidge-jun@users.noreply.github.com>
Preserve supported image inputs in Claude CLI replay and refuse image forms that cannot be represented safely.

Co-authored-by: Nick Davis <inbox@nickdavis.io>
…nd #remote UX (lidge-jun#8255)

* docs(devlog): add docs-first roadmap and PRDs for ocx-child ephemeral link

* feat(link): add ephemeral-child contract helpers and golden conformance suite

* feat(ocx-child): add standalone lightweight Rust Child link relay and offline journal

* feat(link): integrate ephemeral ocx-child discovery, enrollment, and sync fan-out

* docs(link): document ephemeral ocx-child in structure SSOT and docs-site

* fix(ocx-child): persist write-ahead journal, track multiline TOML, and restore user features

* fix(link): harden ocx-child Windows ACLs, strict wire/settings bounds, and discovery precedence

* fix(link): preserve interrupted-sync digests, rollback failed reattach, and align locale/conformance parity

* fix(link): fail fast on unreadable attach snapshots, preserve feature comments, and clarify lifecycle docs

* fix(link): verify restored credentials before relay restart and clarify fan-out restart docs

* docs(devlog): add wp0 roadmap and PRDs for ocx-child plant UX

* feat(link): add ocx-child --purge, conflict codes, and SSH plant foundations

* feat(server): add bare-host SSH plant, rollback, childKind status, and DELETE --purge

* feat(gui,cli): add #remote Ready-to-Plant, parity badges, and --purge UX across 11 locales

* docs(link): document zero-preinstall SSH plant, --purge, and #remote UX

* fix(link): populate PowerShell best-effort catch blocks for pr-hygiene

* fix(link): preserve recovery binary on failed purge rollback and stabilize child.lock inode

* test(link): stage compiled ocx-child with SHA256SUMS for bare-host E2E on CI

* style(ocx-child): remove trailing whitespace

* fix(ocx-child): preserve recovery and verify shutdown

* fix(ocx-child): harden recovery identity and shutdown

* fix(ocx-child): preserve local state during detach and sync

* test(ocx-child): add env-gated test barrier and shared mock Home harness

Process-level lifecycle regressions need to pause a real ocx-child at a
named point. test_hooks::barrier is a no-op unless both
OCX_CHILD_TEST_BARRIER_DIR and OCX_CHILD_TEST_BARRIERS are set; spawned
serve children never inherit those variables.

The mock Home and binary runners move into tests/common so new test
files can share them; the mock gains parity stall/trickle modes.

* fix(ocx-child): serialize and own the shared CODEX_HOME

The lifecycle lock and journal live under OPENCODEX_HOME/child while
config.toml, the catalog and models_cache.json live in CODEX_HOME. Two
ocx-child installs with different OPENCODEX_HOMEs and one CODEX_HOME took
independent locks and could attach, sync and detach the same files with
incompatible journals.

Every writing lifecycle command now also locks a fixed .ocx-child.lock
inside the canonical CODEX_HOME (never deleted) and checks a durable
ownership record (.ocx-child-owner.json: owner id, canonical
OPENCODEX_HOME, directory identities, phase; no secrets). Lock order is
lifecycle lock, CODEX_HOME lock, daemon start gate. A foreign, corrupt or
unreadable record refuses without mutation and names the recovery step;
ownership is never taken over by PID or timeout. Detach releases
ownership last, after a durable cleanup-complete phase, so any crash is
resumable. Journal application and offline detach take the guard as a
parameter.

Regression: tests/lifecycle_guards.rs runs two real processes against one
CODEX_HOME with a barrier inside the protected section.

* style(ocx-child): format lifecycle assertion message

* fix(ocx-child): recheck home identity around every restoration step

Immediate rollback, crash recovery and offline detach verified the home
directory identity once and then wrote and removed files by path name. If
a home was replaced after that check, restoration kept writing into the
new directory and could delete its recovery records.

Every restoration write, rename and removal now goes through
CodexHomeGuard::restore_step, which re-verifies both captured home
identities immediately before and after the call while the CODEX_HOME
lock is held. A mismatch stops restoration, keeps every recovery record
(rollback snapshot, journal, key, ownership record) and reports a
recovery error. The rollback snapshot and journal are removed only by a
step whose checks passed.

Regression: tests/lifecycle_guards.rs pauses detach between verification
and the write, replaces CODEX_HOME with a directory of sentinel files,
and checks the sentinels, the original directory and the records.

* fix(ocx-child): commit a daemon generation before it serves

spawn_detached_serve started the relay, the parent waited for /healthz
and then recorded the PID; attach removed its rollback record later. If
the parent died after handing over the secret, the child kept serving,
including after recovery restored files or after another generation was
committed.

Each start now records a random daemonGeneration in state before the
spawn and passes it with the secret on stdin, which stays open. Inside
the start gate the child requires state.daemonGeneration to match, and
until it reads "commit <generation>" it answers only /healthz and posts
no parity reports. EOF or any other line before the commit makes it exit
without serving. The parent commits only after the state naming the
generation and PID is durable and, for attach, after the rollback record
is removed. Restart, sync rollover and rollback's prior-relay restart use
the same rule. An operator-run serve --foreground is unchanged and is
refused while an attach rollback is pending.

Regression: tests/lifecycle_guards.rs kills the real attach parent at a
barrier before the commit, and starts a daemon with stale and superseded
generations.

* fix(ocx-child): let shutdown end in-flight parity work and a busy pool

The relay joined its parity thread on shutdown, but an in-flight parity
request had only per-read socket timeouts (5 s/15 s), so a stalled or
trickling Home kept it alive well past stop_owned_daemon's 3-second wait
and detach reported that the daemon did not stop. Separately, the accept
loop blocked on the worker semaphore when all 32 workers were busy, so
the shutdown connection was never accepted.

One-shot Home calls now carry a whole-request deadline (60 s for
lifecycle fetches, 10 s for parity) enforced per read. The parity thread
registers its socket in a CancelSlot; shutdown cancels the slot under the
same mutex and shuts the socket down, and serve exits only after the
thread ends. When the pool is full, a connection gets a short-lived
control thread (capped at 256) that answers only /healthz and the
authenticated shutdown with 1-second timeouts.

Regression: tests/lifecycle_guards.rs runs detach against a Home that
stalls or trickles the parity response and against a relay whose workers
are all blocked, and checks the time budget and that the process is gone.

* test(ocx-child): cover removal of an unchanged injected web_search on detach

* docs(link): state the single-owner CODEX_HOME rule for ocx-child

* fix(link): refuse to plant ocx-child when the Child cannot verify its SHA-256

The POSIX plant script skipped the digest check when neither sha256sum nor
shasum existed and then ran the binary after a size check alone. It now
also accepts openssl dgst, and with no hash tool it exits 3 before the
binary is installed or executed, printing ocx-plant-error:hash_tool_unavailable.
Home maps that marker to 502 remote_plant_failed with hint
hash_tool_unavailable. The Windows plant already streams SHA-256 before
running the binary.

* fix(ocx-child): let Home catalog slugs win in models_cache sync after outside edits

A sync after any outside edit to models_cache.json rebuilt the cache by
swapping injected Home entries back to their pre-attach versions, then
rejected each restored entry because it differed from the Home catalog
entry with the same slug. Every later sync failed with
client_models_cache_conflict until detach and re-attach. A Codex rewrite
that re-serialized a Home entry hit the same path.

Injected catalog slugs are now Child-owned by slug: sync writes the Home
version and keeps entries with other slugs, and the detach snapshot keeps
the pre-attach version or drops the slug.

* test(link): give the bare-host plant E2E a SHA-256 tool

The plant script now refuses to install a binary it cannot hash, and the
E2E's bare host exposed only sh and core utilities, so it had been
planting without any digest check. Link the first available sha256sum,
shasum, or openssl into the host's private PATH.

* fix(link): purge through the managed planted ocx-child and report a purge that did not happen

DELETE /api/link/:id {purge:true} ran `disconnect --purge` through the
generic candidate resolver, so a ~/.local/bin/ocx-child could answer
first; it is outside the managed directory, so it exited 0 with
purged:false and Home reported success. The purge was also dropped
silently for links whose record had no childKind.

Home now runs the planted binary at the managed path explicitly
(remoteManagedPurgeDisconnectArgv, POSIX and Windows) with --json and
requires its "purged": true report. A missing managed binary
(managed_child_missing) or a report without the removal
(purge_incomplete) is 502 remote_disconnect_failed and the link record is
kept. A record without childKind is inspected first; a full install
answers 409 purge_unsupported instead of a silent plain disconnect.

* fix(ocx-child): commit a rollback-restarted relay after removing the record

Attach rollback restarted the prior relay and committed it before it
removed the rollback record, so the relay could serve while recovery
might still restore files underneath it. The restart now commits only
after the record is removed; an uncommitted relay is dropped and exits
if the removal fails. The structure doc names this ordering.

* fix(ocx-child): bound each control-mode request head with a deadline

A control connection, accepted while every worker is busy, had only a
1-second per-read timeout, so a sender writing one byte per second held
its slot indefinitely and 256 such senders kept the authenticated
shutdown from being handled. Control mode now reads through the
existing DeadlineStream with a 2-second deadline that covers the whole
exchange, including the shutdown proof frame. The code comment and
structure text now state the bound and the connection cap precisely.

* fix(ocx-child): cap the cancellable parity connect at one second

The parity socket can be registered for cancellation only after it
connects, and that connect could take up to the 5-second auth timeout,
longer than the 3-second wait stop_owned_daemon gives a relay to exit.
A request that carries a CancelSlot now connects with a 1-second bound;
one-shot lifecycle fetches keep the 5-second bound.

* fix(ocx-child): stop re-permissioning existing OPENCODEX_HOME when taking the lifecycle lock

Moving the lifecycle lock to $OPENCODEX_HOME/child.lock made every
FileLock acquisition call ensure_private_dir on OPENCODEX_HOME: attach,
sync, restart, and detach chmodded a shared home to 0700 on POSIX and, on
Windows, applied a protected owner-only DACL with inheritable ACEs that
propagated into a co-installed full OpenCodex tree, all before the
full-ocx conflict check.

FileLock now decides through lock_parent_action: a missing parent is
created private by create_private_dir_if_missing (0700, or a hardened
DACL only on a directory this call created), an existing parent is used
as it is, and a symlink or non-directory is refused. The lock file itself
is still created 0600 / owner-only on the file alone. CODEX_HOME keeps
using lidge-jun#8237's acquire_in_existing_dir, which never touched its mode.

* fix(link): treat an empty OPENCODEX_HOME as unset when looking up cached ocx-child artifacts

OPENCODEX_HOME= made the cache lookup join(, cache, ...), a path
relative to the server's working directory. An empty or blank value now
falls back to ~/.opencodex, as ocx-child does. structure/remote-link.md
also records that childKind joined the strict link record keys, so a
downgraded Home rejects a links.json that carries it (the same trade-off
as remotePlatform).

* fix(ocx-child): keep both catalogs in the backup across the cache write

sync replaced the catalog backup with the new catalog before it wrote
models_cache.json. An I/O error or crash between the two left a cache of
previous-catalog entries next to a backup naming only the new catalog,
so the next reconcile kept those previous Home entries as local ones and
detach restored them into the native cache.

Before the cache write the backup now names the slugs of both catalogs;
after it, the new catalog alone. The regression interrupts a sync with
JournalWriteFault::AfterCatalogWrite and checks detach, directly and
after a retried sync.

* fix(ocx-child): remove the padding inside the relay restart hint

A source line wrap had put about 20 spaces between "run" and "ocx-child restart" in the message attach prints when its relay does not start.

* fix(ocx-child): compile the test barrier out of release builds

test_hooks::barrier read two environment variables on every lifecycle
path and the env names were linked into the release binary. The barrier,
its env constants, and the env stripping for a spawned relay now exist
only with debug_assertions, the profile cargo test builds the binary
with; a release build compiles them to no-ops.

* docs(link): say where a plantable ocx-child build must come from today

The guide claimed a bare SSH host works out of the box, but
resolveChildBinarySource only finds a build in OPENCODEX_CHILD_BIN_DIR,
native-bin/ sidecars (no package ships them), a source checkout's own
cargo release build, or a SHA256SUMS-verified
$OPENCODEX_HOME/cache/ocx-child/<version>/ (nothing fills it). A packaged
npm or desktop Home therefore reports plantable:false with
artifact_not_found.

The English guide and structure now list those sources in order, state
that packaged installs need one of them, and describe the Child's hash
tool requirement. When the probe reports artifact_not_found, the CLI,
the remote_plant_unavailable message (plantUnavailableGuidance), and the
#remote sheet (link.plant.artifactHelp in all 11 locales) name the file
and the directories Home reads.

* fix(ocx-child): open lock files without following links

FileLock checked the path with reject_symlink and then opened it with
create(true), so a symlink swapped in between made the open follow it
and create the link target. The open now uses O_NOFOLLOW on Unix, with
the value libc defines for each target, and FILE_FLAG_OPEN_REPARSE_POINT
on Windows, and refuses anything that is not a plain file. On Windows an
existing lock file is verified to carry the owner-only DACL instead of
being accepted unchecked. A debug-only barrier between the check and the
open lets the regression swap in the symlink.

* fix(ocx-child): wipe the shutdown secret and token-bearing request heads

The shutdown secret was a plain String in ChildStateV1, so every state
read, clone, and drop left a copy in memory and Debug printed it. It is
now a SecretString that is wiped on drop, serialized as the same plain
string, and redacted from Debug; load_live_creds no longer hands out a
copy nobody used, and the rollback records no longer wipe it by hand.

The daemon start line and the relay and lifecycle request heads that
carry the link token are built once at their final size into
SecretBytes, and state.json bytes are wiped after a read or write.

* docs(link): state Child sync serialization and the restore window precisely

The lifecycle reference said a second ocx sync fails fast on any Child.
That holds for a full ocx Child; on ocx-child only the --expect-link form
the Home's fan-out runs exits 5 with pending_operation, and a plain
ocx-child sync waits for the first apply. The structure note now names
who can act in the restore window, and the ocx-child status ledger no
longer carries stale test counts or a stale wp5 summary.

* test(link): find the ocx-child binary under CARGO_TARGET_DIR

The e2e test built ocx-child with cargo but always looked for the binary under native/ocx-child/target, so a run with CARGO_TARGET_DIR set failed with ENOENT. It now resolves the target directory the way cargo does.

* fix(ocx-child): stop printing the link key id after attach

The attach success line named the link key identifier. Keep identifiers out of console output, matching the repository privacy rule; the regression asserts attach output never contains the key id.

* fix(link): keep an unrunnable planted ocx-child while any recovery record exists

The enrollment-rollback purge deletes a planted ocx-child that cannot run
unless recovery evidence remains, but it only looked for child/state.json
and codex-journal.json. An attach interrupted before state.json exists
leaves only attach-rollback.json and the CODEX_HOME ownership record
(.ocx-child-owner.json); the purge then deleted the one binary that could
roll that attach back and reported success.

Both the POSIX and the Windows purge now treat the attach rollback snapshot
and the ownership record (under CODEX_HOME, defaulting to ~/.codex like
ocx-child) as recovery evidence, keep the binary, name the record on
stderr, and exit non-zero so Home reports a residual.

* fix(gui): explain a refused --purge on a full install in the #remote sheet

DELETE /api/link/:id with { "purge": true } answers 409 purge_unsupported
when the Child turns out to run a full OpenCodex install, but the GUI had
no mapping for that code and showed the generic "could not be completed"
text. Map it to a message in all eleven locales that says there is no
planted binary to delete and to disconnect without deleting it.

The route-owner scrape now also reads the codes purgeDisconnectRemote
returns, because the DELETE route forwards them as a variable.
Reconcile streamed sidecar text snapshots by compatible part identity while preserving output order and bounded processing.

Co-authored-by: 정우철 <oocheol@naver.com>
lidge-jun and others added 24 commits November 9, 2026 20:13
…alling it foreign (lidge-jun#8439)

* fix(usage): wait for a concurrently created filter index instead of calling it foreign

* fix(usage): recheck a stale filter index schema under the lock and document the open contract

* test(usage): release the still-creating lock from the reader's busy back-off, not a timer

* docs(usage): state the four-attempt busy bound the reviewer measured
…process already has (lidge-jun#8441)

* test(clients): give each Desktop lifecycle case the budget its child process already has

* test(clients): give the sibling runEvalChild cases the same child-process budget

* test(clients): stop the Desktop lifecycle sync fixture from probing the host Codex CLI
Refresh the documentation landing page, navigation and theme while preserving routes and keyboard access. Keep the responsive header usable across locales, short viewports and reduced-motion preferences.

Co-authored-by: 이완우 <theguide123456789@gmail.com>
…target (lidge-jun#8438)

* fix(gui): scope provider drafts to their dashboard target

Provider Settings and the raw JSON editor kept one draft per provider name,
while the Providers page can outlive an apiBase change. A same-name provider
on the next target could inherit the old draft, or the old draft could be
saved through the new target's callback.

Keep drafts and their saved baselines in an in-memory store keyed by target
and provider. The form remounts per target, config is bound to the target
that answered it, and a save completion is fenced to the target that started
it. Returning to a target restores its draft; if its saved values moved, the
UI asks for an explicit keep-draft or use-saved choice instead of silently
overwriting either side. Nothing is written to browser storage.

Adds the target label and conflict banner to both Settings and JSON, with
translations, docs and regression tests.

* fix(gui): rebase provider drafts onto the latest target state

Keep-draft now applies only the user's delta to the latest saved config:
Settings patches changed fields only, and the JSON editor performs a
three-way merge that preserves unrelated providers and fields and reports
same-field, deletion and rename conflicts explicitly. Restore returns to
the latest saved values, and a no-op save never sends an identical
baseline/next pair.

Fence the logical dashboard target (hub origin plus machine key id) across
config, drafts, OAuth status, account rosters and login/logout completions,
so a relay re-pair cannot reuse the previous hub's state. An old target's
save completion can no longer advance the shared config request sequence
and strand the new target in loading.

Clear parked drafts on a confirmed provider removal and on leave-dialog
discard, keep edits typed while a save is in flight, and sync the endpoint
picker when saved values move.

* docs: record audited provider draft target plan (lidge-jun#8325)

* feat(gui): bind provider drafts to stable targets and relay saves

Stable logical identity is hub origin plus link id (not API-key rotation),
with App discovery coalesced across session/focus/mismatch events and the
draft owner kept above remounts. Draft entries use a mount owner CAS so a
stale save completion cannot write or clear a newer instance; discard,
removal and A-B-A retain only same-instance newer edits.

Settings and JSON now merge only the user delta into the latest observed
saved values: deep pacing subfield merge, partial JSON merge with
edit-to-resolve conflicts, latest-value Restore, numeric bounds, and a
no-op save that never sends identical baseline/next. Endpoint choice is
synced when saved values move.

The machine listener advertises expected-target-v1 and the hub relay
validates a bounded canonical expected origin against the same immutable
forwarding snapshot before reading the body, strips the field, and keeps
legacy absent-field behavior with an explicit preflight notice. GUI
mutations capture their target before auth and refuse a changed target.
Usage, preset, Dashboard and Codex-pool caches carry the logical identity.

* test(gui): keep API-key rotation outside logical target identity

* fix(gui): keep prototype keys as data and strip composite labels

* fix(gui): fence every mutation transport and add target transport witnesses

* test(gui): configure the captured target in CRUD transport tests

* fix(gui): retain login cancellation capture and add lifecycle witnesses

* test(gui): exercise own-key merge through the object path

* fix(gui): capture manual-code and Kiro profile mutations

* fix(gui): fence remaining provider mutations and target-scoped UI state

* test(gui): prove panel and OAuth target capture isolation

* fix(gui): keep provider actions on the target they were captured on

An embedded component supplies its own apiBase. While the dashboard has no
discovered topology that base is the target; discovery still wins, so a caller
cannot retarget a configured relay. Unknown relay capability strings read as
legacy instead of failing discovery.

OAuth cancellation now always uses the capture the login started with, and
fails closed when it is gone, so a timeout or rejection from the old target
cannot cancel a login on the new target. Logout failures are guarded before
any notice. Codex pool mutations fence every post-await write by the captured
generation and the rendered base, including pending reconciliation refs.
target_changed responses render the localized target message.

* test(gui): prove target capture, cancellation and completion fencing

Cover the embedded standalone base contract and its discovery boundary, an
unknown relay capability reading as legacy, and the expected-target header
surviving a 401 retry. Add deferred-completion witnesses for a switch and a
pause accepted on the old target, plus OAuth cancel and logout settlement
after a target change. The pin-release source assertion follows the
present-checked confirmation row.

* docs(structure): record the embedded target and legacy capability contract

The dashboard provider row now states that an embedded apiBase is authoritative
only before discovery, that a configured relay wins, and that an unrecognized
relay capability reads as legacy.

* test(gui): make the target-change witnesses discriminate

The OAuth cancel witness now runs on a relay target so the live capture differs from
the login capture, and it passes stable setters so the pagehide effect does not rerun
on every render. The pause witness gates the completion follow-up load, which had
been correcting the stale write before the assertion.

* fix(gui): send pool settings through the captured target

The global auto-switch threshold and the Claude account-pool settings still
used a bare fetch, so a relay request carried no expected target and a
completion could apply after a switch. Both now capture at the action,
send through targetFetch and check the capture before applying. The
Claude pool keeps its existing apiBase/provider request guard.

* fix(gui): own the target at the form and dialog boundary

Settings computed a patch and let the update callback capture the live target,
so a module target change without a re-render sent the save to the new hub.
The form now owns a render capture and passes it through a typed
onUpdateProvider(name, patch, capture); the detail pane injects it into every
child call and the CRUD hook owns its own for remove, disable and default.
A render-owned capture may still reach its own target, where the relay refuses
the stale expected target; completions stay fenced by the strict check.

A remove confirmation captures at open and refuses after a target change.
Key remove, add and alias guard their completions; the add-key form and the
Add Provider completion do not survive a switch. Kiro polls with the session
capture and ends when it is stale. CodexSet threads the logical target.

* test(gui): keep the remove confirmation across the target change

The witness now re-renders the same hook instance on B and back to A, so the
pending confirmation is exercised instead of a remount. A refusal keeps the
pending capture until a delete succeeds, so an A-B-A confirm still refuses.

* docs: record capture ownership at the form and dialog boundary

The acceptance plan and the dashboard structure note now state that the form
owns the capture, the typed update callback carries it, and a remove or mode
confirmation keeps the capture from when it opened.

* test(gui): follow the Codex Set route's new target prop

Codex Set now receives the logical target so its account pool is scoped like
Providers. The source assertion follows that contract; the route is unchanged.

* fix(gui): refuse every stale capture before fetch

Restore the audited contract: a stale capture is refused before authentication
or fetch regardless of who created it. The render-owned capture changes the
capture point, not the fence; the permission exception is removed.

The detail pane forwards a caller capture (capture ?? updateCapture) instead of
replacing the form's. An empty targetKey falls back to apiBase so embedded
surfaces do not share one identity. The relay capability is read from the live
target after the identity check, so a same-identity downgrade uses the fresh
legacy preflight and a downgrade with a target change refuses before fetch.

* docs: correct the render-capture contract in the acceptance plan

The render-owned capture changes only where the capture is created. Every
stale capture is refused before authentication or fetch; the relay capability
is read from the live target after the identity check. This corrects an
implementation drift from plan revision 4 without changing the architecture.

* fix(gui): own the target in every provider capture owner

The account, OAuth, Kiro, Codex pool, auto-switch and Anthropic pool hooks
created their capture inside the action, so a module target change that had not
re-rendered the surface captured the live new target and the strict transport
guard accepted it. Each hook now creates the capture at render with
useRenderTargetCapture and carries it through loads, actions and completions.

The logical target is threaded through ProviderAuthPanel, the Kiro dialog, the
Anthropic pool card and CodexAccountPool. The leave dialog captures its target
when it opens, closes on a target change, and uses the form's owner-checked
discard. A parameterized render-gap suite covers every capture owner.

* fix(gui): keep the render capture server-render safe

Server-rendered surfaces have no window or session. The capture hook now
returns an inert capture there instead of reading the location. The plan
records the render-owned capture owners and the leave-dialog boundary.

* fix(gui): adopt saved provider settings without lint suppressions

The Settings form reset wrote state from a useEffect, which the React Compiler
lint rejects as EffectSetState. The PR silenced it with an eslint-disable for
react-hooks/set-state-in-effect and an oxlint-disable for react/react-compiler
on the effect, plus an exhaustive-deps disable for the render-local applyDraft.
Those added suppressions fail the deterministic PR hygiene gate
(new_suppression), and react-doctor reports the same flow as
no-adjust-state-on-prop-change and no-effect-chain.

Adopt saved values while rendering instead, the React pattern for a
prop-driven reset: a clean form follows the saved item in the commit that
delivers it, and a dirty draft keeps what the user typed so the conflict
banner can ask which side wins. Account mode uses the same pattern, kept
separate so a mode PATCH refresh cannot reset an in-progress draft.

The draft-store owner token becomes a lazily initialised state value instead
of useRef(Symbol(...)), which rebuilt the symbol on every render and read a
ref during render. The preset read no longer needs its dependency suppressions
because it already reads the current base URL through a ref.

Verification: gui oxlint, tsc -b, root typecheck, structure:check, file-size
and repo-hygiene guards, privacy scan, pr-hygiene helper on the PR diff
(no failures), and the provider settings/draft/pacing suites.

* fix(gui): clear the remaining react-doctor findings on the target-scope PR

The React Doctor PR gate runs react-doctor@0.9.11 with `--blocking warning` over
the changed files. After the Settings reset fix, 48 findings remained across the
target-scope surfaces and their tests. Each one is fixed at its source; no rule,
threshold, baseline, or suppression changed.

- App.tsx: the target-discovery effect paired its listeners in a `for...of`
  loop, which the cleanup analyzer cannot follow. The same three events are now
  registered and released explicitly, keeping `discovery.dispose()` first.
- useCodexAccountPool: the target reset clears rows while rendering, so the new
  target never paints the previous target's accounts; the generation guard and
  the loaded/initial-load refs still move in the effect, because in-flight loads
  read them. `captureIsCurrent` becomes a stable `useCallback` over the apiBase
  ref, so the eight action callbacks can list it instead of closing over a
  render-local function.
- useProviderAccountPools: the bounded roster read moves to a module helper.
  A `try`/`finally` inside the memoized callback makes the compiler lose its
  dependency inference and report the capture the read uses as an extra
  dependency; the callback is now a plain delegation.
- use-providers-fetch: the post-read supersede check becomes a named predicate.
  It is only meaningful after the read settles, and naming it says so.
- Test harnesses publish hook results from a layout effect instead of writing
  them to module bindings during render, which the compiler rejects. Assertions
  and the values they read are unchanged; the crud harness drops the ready flag
  that existed only to force a second render, and the transport test now uses
  the capture it takes, which is what its name claims.

Verification: react-doctor 0.9.11 over all 75 changed gui files reports 0
errors and 0 warnings (exit 0); gui oxlint and `tsc -b` pass; root typecheck,
structure:check, file-size and repo-hygiene guards, and privacy scan pass;
the affected gui suites pass; the PR hygiene helper reports no failures.

* fix(gui): bind Codex login and reauth flows to their logical target

A relay keeps one apiBase across a re-pair, so the Codex add-account OAuth and
native-main device reauth hooks could send A's flow id, device code or cancel to
B after a target change. Each flow now stores the render capture it started on,
sends every start, status, manual-code and cancel request through targetFetch,
and applies a completion only while that capture is still current. The modal,
pool frame, Providers modal and Codex Set Multi-auth panel are keyed by the
logical target and close or reset target-scoped state when it changes.

Also fences the pool's quota-activation PUT and read-back with the same capture,
and shows the existing legacy-preflight notice in Codex Set through a shared
hook instead of duplicating the listener.

* fix(gui): clear react-doctor findings on the auth target-capture delta

Reset the visible main-reauth state at the prop boundary instead of inside the
target-change effect, while the effect keeps retiring the original flow on its
own capture. Publish the test hook/state globals from layout effects and bind
the OAuth callbacks through a stable local so the pinned react-doctor scan
reports no new diagnostics.

* fix(gui): fence late account-pool reads on their captured target

dd130 cleared the previous target's rows while rendering but left the generation
bump in a passive effect, so a read issued for A that settled between B's commit
and that flush still matched the generation and repainted A's rows, active id and
ready state under B. The relay re-pair (same apiBase, hub-a -> hub-b) is the case
that exposed it.

Every apply, catch, settle and counter write now checks the captured target as
well as the generation, and a load whose render no longer owns the target does not
start. The render identity is published from a layout effect because the
continuation runs after the layout phase and before passive effects.

Verification: the reviewer's held-read repro is red on ef6cdf6 and green here;
the new gui regression releases A from B's layout phase and asserts B's rows,
active id, load state and first-attempt flag. gui pool and target suites 126 + 67
pass; gui oxlint 0/0; gui build passes; root typecheck, privacy scan,
structure:check, repo-hygiene and file-size guards pass; react-doctor 0.9.11 over
base c81be32 reports no changed-scope issues and full scope stays at the base's
79 pre-existing findings.

* fix(gui): scope the account-pool in-flight counter to its reset

The counter fence compared the seed key, which cannot tell A -> B -> A apart:
the first A read's increment was discarded by the render-phase clear, yet its
completion still matched the key and decremented the second A read's count, so
refreshing went false while that read was still held.

Tag each load with the reset epoch the clear advances, publish it from the same
layout effect as the seed key, and release an increment only under the epoch that
took it. A read superseded by a newer read on the same target keeps its own epoch
and still releases its count, so the fence cannot be loadIsCurrent alone.

The validation pause lease is unchanged: it is a Set, so its own finally's delete
is idempotent and pauseCount is read from the live set. An old target's lease can
still hold the new target's poll for the rest of its budget; that is pre-existing
and outside this fix.

Verification: the reviewer's ABA probe is red on 2702695 and green here; the
new gui tests cover the ABA counter (red before this commit) and the same-target
overlap release. gui pool and target suites 195 pass; gui oxlint 0/0; gui build
passes; root typecheck, privacy scan, structure:check, repo-hygiene and file-size
guards pass; react-doctor 0.9.11 changed scope over base c81be32 reports no
issues.

* fix(gui): reset the account pool's target-owned refs on a target change

The render-phase clear emptied the rows but left two refs from the old target: a
pending switch's reconciliation marker and the last /active payload. On the new
target the marker spent its one stale-read allowance on the first /active answer,
so the active id stayed empty until the next poll and the main account could look
active. The payload seeded the old target's threshold and strategy into the new
surface through readLastThreshold and readLastActive.

Clear both in the layout effect that publishes the render identity, ahead of the
new target's initial load and of any stale continuation, and key the last payload
by the target that answered so a descendant reading before that effect still
cannot see the old one.

Verification: both new gui tests are red on d53fc13 (active id null instead of
b6; readLastActive returns a7's payload) and green here; gui pool and target
suites 197 pass, strategy and auto-switch consumers 92 pass; gui oxlint 0/0; gui
build passes; root typecheck, privacy scan, structure:check, repo-hygiene and
file-size guards pass; react-doctor 0.9.11 changed scope over base c81be32
reports no issues.

* fix(gui): scope the account pool's mutation gates to their target

The gates were process-wide: a switch, pause, order, auto-switch or credits write
still held its ref across a relay re-pair, so the new target's action came back
"busy" and the new target painted the old one's spinner. The release also
compared the account id, which cannot tell two targets apart when they share one.

Each gate now holds a MutationLease tagged with the target epoch. An action whose
render predates the current epoch, or whose capture is stale, refuses before it
touches the gate; a lease from another epoch does not block the new target; and
only the exact lease that took the gate may release it. The target transition
clears the visible busy state, so the new target starts idle.

Verification: seven parameterized gui tests (switch, pause, priority, auto-switch,
per-account credits, global credits, pause-exhausted) use the same account id on
both targets, are red on ba8990b with the old target's busy value painted on
the new one, and green here; each also asserts the same-target gate still answers
"busy" and that the old target's completion leaves the new target's gate held.
gui pool and target suites 258 pass; gui oxlint 0/0; gui build passes; root
typecheck, privacy scan, structure:check, repo-hygiene and file-size guards pass;
react-doctor 0.9.11 changed scope over base c81be32 reports no issues.

* fix(gui): key a render capture by its request base, not only the logical target

useRenderTargetCapture memoized on targetKey whenever one was supplied. A relay
that re-pairs to the same hub keeps that key (the hub origin) while the request
base and transport move from <machine>/api/machine/hub-relay to the hub itself, so
the surface kept the relay capture and every targetFetch refused it until a
remount.

Include apiBase in the memo identity. The logical key still names the capture and
the draft store, so a same-hub transport change does not discard a draft, and a
module target change with unchanged props still keeps the old capture and sends
nothing.

Verification: the new relay-to-direct test is red on 8848ba5 (the old relay
base is returned) and green here; the no-prop-change test holds the old capture
and targetFetch answers 409 with zero requests in both. Focused capture, pool and
target suites 298 pass; gui oxlint 0/0; gui build passes; root typecheck, privacy
scan, structure:check, repo-hygiene and file-size guards pass; react-doctor 0.9.11
changed scope over base c81be32 reports no issues.

* fix(gui): localize target_changed and drop stale pool action toasts

Two generic paths still surfaced the raw wire token: the provider CRUD error
helper and the key, alias and login callers read data.error directly, so a relay
409 showed "target_changed" instead of the localized copy. They now share
errorMessageFrom, which maps that one token and passes every other code or
message through unchanged.

The pool's alias and remove handlers also reported a stale refusal as a failure
toast on the new target. They now drop busy and stale the way pause, order,
threshold and credits already do. The controller still returns stale before it
reloads, so the new target's rows are untouched.

Verification: the three pool caller tests show "target_changed" on abd4109 and
the localized copy here; the alias and removal component tests show their failure
toasts on abd4109 and none here, with the read count unchanged across the old
write's completion. Focused suites 241 pass; full gui suite 4343 pass, 0 fail;
gui oxlint 0/0; gui build passes; root typecheck, privacy scan, structure:check,
repo-hygiene and file-size guards pass; react-doctor 0.9.11 changed scope over
base c81be32 reports no issues.

* fix(gui): fence provider pool reads on their captured target

The provider pool updated serverRef in a passive effect and cleared the
previous target's rows in a deferred microtask, so a roster, key or quota
read issued for A that settled between B's commit and that flush still
matched the generation and repainted A's emails, key labels and quota
under B. accountSets and keyPools were also returned ungated, so the old
target's rows could paint for a frame even without a late completion.

Publish the render's target from a layout effect, require the captured
target on every read, roster, quota, apply, catch and completion
predicate, and clear the target-owned rows while rendering so the new
target never paints the previous one. Same-target singleflight and the
false obsolete result are unchanged.

Verification: the reviewer's held-read probes are red on b244ed8 and
green here; the new gui regression covers account and key late reads,
held reads on the new target, an ABA round trip, late quota, late
failure and a late account switch.

* test(gui): wait for the target-change passive flush, not a timer

The held-read-on-the-new-target case started the new target's read after a
setTimeout(0). When the target-change passive cleanup had not flushed yet,
that cleanup bumped the generation under the in-flight read, so a
legitimate new-target query returned false. The same timer also made the
P2 release window depend on scheduler order.

Wait for the commit's passive effect itself: the probe's effect is
declared after the hook, so its resolution is the barrier after the
hook's cleanup and setup. The mount path no longer needs a timer either.
…stop churning the config directory (lidge-jun#8442)

* fix(usage): keep the filter index's WAL sidecars so filtered queries stop churning the config directory

* fix(usage): keep WAL sidecars only for this projection and truncate the kept WAL on close

* fix(usage): checkpoint the kept WAL with TRUNCATE before the writer closes

* fix(usage): register kept WAL sidecars beside a projection built before they persisted

* fix(usage): keep checking sidecar ownership until both sidecars exist

* fix(usage): register a crash-left projection file with its kept sidecars
…ds (lidge-jun#8459)

* test(usage): name the input that moved when the warm aggregate rebuilds

* test(usage): report the previous stage's recorded base and the filtered reads
…hem on failure (lidge-jun#8458)

* test(rust): derive toolchain homes from the rustup proxy and report them on failure

* test(rust): derive each unset toolchain home independently and report the default toolchain
…-jun#8463)

* ci: refresh the Windows shard durations from batch 42694893702

* ci: pin the refreshed Windows duration table in the release evidence policy

Appends a ci policy entry identical to the latest one except for the new
scripts/ci/test-durations.windows.tsv blob, keeping earlier entries so
historical revisions still verify.
…dows ChangeTime move (lidge-jun#8462)

* test(usage): settle the per-write config ACL harden before recording the warm base

* test(usage): read the warm base after the stage's work and name a Windows ChangeTime move

The settle added before the refreshed aggregate drained no registered work:
every config-directory harden it targets fires at startup and is already
flushed there, and the per-save directory harden is synchronous inside the
write. The batch payload only shows usage.jsonl's ChangeTime alone moving
after refreshedAggregate recorded its revision.

Read the base the next stage compares against after all of the stage's work.
If that read is not warm, accept it only on Windows, only as a rebuild whose
revision differs in ChangeTime alone with overlay version and time zone
unchanged, and log the ChangeTime after each step so the next batch names the
mover.

* test(usage): require the tolerated Windows rebuild to show a ChangeTime move

A rebuild whose revision is identical to the refreshed one, such as a
discarded retained aggregate, no longer passes as a Windows ChangeTime move.
…dge-jun#8465)

macos 3/4 hit its 20-minute limit in dev batch 42721779341 because the
shared table was read from Ubuntu logs only. Refresh it from the passing
test N/5 and macos N/4 jobs of run 42694893702, as the durations tool
prescribes, and pin the new table in the release evidence policy.
… rows (lidge-jun#8466)

* feat(anthropic): add Claude Fable 5.2 pricing, fast mode and provider rows

Anthropic released Claude Fable 5.2 (claude-fable-5-2) on 2026-11-09. Without static rows the
picker showed it with no price, max output or effort ladder, and its fast mode was not offered.

- Anthropic seed: 1M context, 128K output, low..max effort, text+image input.
- Fast mode: claude-fable-5-2 joins the anthropic-speed allowlist (Fable 5 and 5.1 still reject
  speed) with the 2x, response-confirmed priority price rule (16 / 80).
- Price overlays at 8 / 40 / 0.20 cache read (0.025x) / 10 cache write for anthropic,
  anthropic-apikey, cursor (Cursor model page), devin and devin-cli (Devin model page).
- Metadata snapshot + regenerated table: anthropic; Bedrock anthropic./global. (list) and us.
  (1.1x); openrouter (and its repriced fable-latest alias); source rows for github-copilot,
  kilo, vercel (+fast), venice and opencode-zen.
- Opper pool and Devin static seeds; docs list Fable 5.2 among Anthropic Fast models.

Cursor gets a price overlay only: its wire ids are not on the live roster yet, so no
preemptive capability row. Sources: platform.claude.com Fable 5.2 overview, what's-new and
pricing; cursor.com, docs.github.com, opper.ai and docs.devin.ai model pages; OpenRouter,
Vercel, Kilo, Venice and models.dev APIs. Plan and audit: devlog/_plan/261110_fable_5_2_catalog/.

* fix(usage): price Kilo fable-latest and Vercel Fable 5.2 fast; move Fable 5.2 pricing test to a sibling

Kilo and Vercel are not generated metadata bundles, so their snapshot rows never reach the
resolver; explicit overlays price both ids. The Fable 5.2 pricing cases move to
tests/usage/usage-fable-5-2-pricing.test.ts so usage-cost.test.ts stays under the 2000-line
ratchet, and the client-export roster now expects the Fable 5.2 fast selector.

* docs(devlog): record Fable 5.2 catalog unit outcome

* docs(providers): list the Fable 5.2 confirmed-fast rate
…-jun#8467)

npm read the bare npm-artifact/package.tgz spec as a GitHub shorthand and ran git ls-remote,
so the first dev-line publication (run 42763073267) failed before publishing anything.
…line (lidge-jun#8468)

* fix(release): start the 2.100.0 changelog at the reviewed legacy baseline

v2.99.0 was tagged on the retired main line, so the changelog fell back to the newest
reachable stable, v2.42.0: a 4,960-commit range that exhausted the GitHub API and would
exceed the 125,000-character release body. The reviewed legacy mapping in
release-history.cjs now admits v2.99.0 and starts the range at its dev baseline.

* fix(release): keep generated notes and the compare link inside the legacy range
…idge-jun#8469)

* fix(anthropic): rank Fable accounts by its share of the weekly limit

Claude's usage endpoint now reports weekly_scoped_shares, naming by raw
limits index the share of the weekly limit a scoped family may consume
(Fable: "up to 50% of your weekly limit"). The Fable window now carries it
as weeklyAllowancePercent, and lowest-usage pool ranking restates Fable on
the weekly scale, so an account at 60% Fable with 20 weekly points left no
longer outranks one with 30. Auto-switch thresholds, exhaustion and route
headroom keep their existing meaning.

* fix(anthropic): keep five-hour ranking order for fresh Fable seats

Five-hour pool ranking has no five-hour tie-break, so the weekly-scale Fable figure (50 for a fresh seat) masked every lower five-hour reading. Apply the conversion only under the weekly and max-utilization windows.

* fix(anthropic): carry the weekly allowance only onto header readings

A usage probe that reports no share is itself the current reading, so the merge must not restore the previous allowance onto it.
lidge-jun#8433)

* fix(responses): reject malformed retained compact input

* test: strengthen malformed compact envelope regression coverage

* test: strengthen malformed compact envelope regression coverage

* test: strengthen malformed compact envelope regression coverage

* fix(responses): reject malformed retained compact input before credential refresh

Co-Authored-By: Epinephrine <luvs01@hanmail.net>

* docs: clarify malformed compact input in translated references

* docs(responses): document retained envelope helpers

Co-Authored-By: Epinephrine <luvs01@hanmail.net>

* docs(responses): document the compact route entrypoint

Co-Authored-By: Epinephrine <luvs01@hanmail.net>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…omain (lidge-jun#8472)

The zone route needed an existing proxied DNS record for telemetry.opencodex.me, which does not
exist, so a deploy left the hostname unresolvable and every client request failed. A Custom Domain
creates the record and certificate on deploy.
…n#8401)

* fix(sidecars): preserve identities across partial snapshots

* fix(sidecars): update lookup only for newly learned indexes
…rences (lidge-jun#8434)

Bound Mistral image pruning bookkeeping and replace malformed image references before dispatch.

Co-authored-by: Epinephrine <luvs01@hanmail.net>
…arry of lidge-jun#8084) (lidge-jun#8473)

* fix(kiro): request Identity Center profiles within the AWS page limit

ListAvailableProfiles rejects maxResults above 10 with a 400 in every
region, so every Identity Center login failed after AWS approved it. The
test mock accepted any request; it now rejects an oversized page too.

* fix(kiro): list Identity Center profiles without maxResults

Maintainer follow-up to the carried fix from lidge-jun#8084. AWS rejects maxResults above
10 (400 ValidationException, observed in lidge-jun#8084), and another Kiro client reports
400 REQUEST_BODY_INVALID for any maxResults while an empty body works
(Quorinex/Kiro-Go#164). Sending no page size and paginating with nextToken, as the
code already does, works under both observations. The fixture now rejects any
maxResults, so dev's 50 and a fixed 10 both fail the discovery tests.

Co-authored-by: Mushikingh <164845020+mushikingh@users.noreply.github.com>

* test(kiro): pin successful IAM profile pagination

Page two must carry only nextToken (no maxResults) and both pages must be
merged. Fails if maxResults returns on the continuation or nextToken is
dropped; the existing tests covered only the first page and cycle rejection.

Co-authored-by: Mushikingh <164845020+mushikingh@users.noreply.github.com>

---------

Co-authored-by: Mushikingh <164845020+mushikingh@users.noreply.github.com>
Co-authored-by: lidge-jun <lidge-jun@users.noreply.github.com>
… wait (lidge-jun#8422)

Revalidate ownership after a spent publication wait and avoid caching a wait under a changed identity.

Co-authored-by: Epinephrine <luvs01@hanmail.net>
…-jun#8083)

* fix(venice): upscale inline images below the 64px minimum edge

* fix(venice): bound and cancel minimum-edge upscales; keep openai-chat under its cap

Maintainer carry on top of the contributor's Venice fix:
- Move the image-preparation decision into prepareOpenAIChatImages
  (openai-chat-images.ts) so src/adapters/openai-chat.ts stays under its 822-line
  file-size cap; the registry lookup now runs once per adapter, not per request.
- Upscales take the process-wide decode gate (enterImageDecode) shared with the
  other image normalizers, so concurrent Venice requests cannot each hold a decoded
  bitmap.
- A cancellation during the last resize now rejects instead of building a request
  for a cancelled turn.
- Two regression tests; both fail without this change.
- Structure note: the aspect ratio is kept unless the 4096 clamp applies.

Co-authored-by: tachyon6 <tachyon6@users.noreply.github.com>

* docs(structure): state the Venice upscale aspect-ratio exception plainly

Co-authored-by: tachyon6 <tachyon6@users.noreply.github.com>

---------

Co-authored-by: lidge-jun <lidge-jun@users.noreply.github.com>
Co-authored-by: tachyon6 <tachyon6@users.noreply.github.com>
…idge-jun#8474)

Anthropic refuses claude-fable-5-2 below Claude Code 2.1.290 ("Claude Code 2.1.280 does not support
this model"). The proxy-generated identity now presents 2.1.321 with @anthropic-ai/sdk 0.128.0 on
runtime v26.12.1, the same baseline router-for-me/CLIProxyAPI#7392 measured for the identical 400.
Integrate the tagged stable release while retaining fork usage retention,
transaction diagnostics, Hub entitlement querying, and signed releases.

Reconcile dispatch and ledger contracts, preserve dependency pins, and
align all version sources with the release tag for signed package binding.
Split generated telemetry guidance to retain the chapter size contract.

Signed-off-by: Bernardo Donadio <bcdonadio@bcdonadio.com>
@bcdonadio bcdonadio self-assigned this Nov 10, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-10T22:07:55.718459Z 9e58505 PR opened
🔒 Security Review ✅ Completed 2026-11-10T22:18:23.080952Z 9e58505 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

The stable update adds ocx-child sources to the package manifest. Include
those files in the restricted Docker build context and package stage, and
require them during archive verification to prevent incomplete releases.

This ships source parity only; child execution still needs its expected
prebuilt artifact. No compiler or package scripts run during packing.

Signed-off-by: Bernardo Donadio <bcdonadio@bcdonadio.com>
Keep the child source inventory contract in the existing release paragraph
so the upstream structure documentation size guard remains satisfied.

Signed-off-by: Bernardo Donadio <bcdonadio@bcdonadio.com>
@bcdonadio
bcdonadio merged commit e2b5ed9 into custom Nov 10, 2026
12 of 14 checks passed
@bcdonadio
bcdonadio deleted the update/upstream-2.100.1 branch November 10, 2026 22:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.