You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Adopt pre-substrate Codex homes into the write coordinator #1049
The Codex write lock now has a production caller: injectCodexConfig holds it across the journal write, both artifact replacements, and the injected-state marking, with real two-process contention proven.
But coordination only covers clean first applies and homes that already have a valid coordinator. A home that was routed before this substrate existed keeps its previous, uncoordinated write path.
That is not a rare case. It is every install that predates the substrate.
Why it cannot simply be turned on
assertInitialStateCanBeCreated refuses to create the first coordinator row while native routing residue exists (src/codex/transition-state.ts:268-280). The refusal is correct: installing {0,null} over routed bytes erases the only evidence that an interrupted transition still needs salvage.
So the operation that would create the row is exactly the operation being refused. Gating injection on the lock without solving this would have broken re-injection for the entire installed base — found during implementation, not during five rounds of design review.
The current code decides eligibility before attempting acquisition, so the refusal path is never entered and those homes keep working.
What adoption needs
The design already exists: adoption-pending publication is specified at 005_contract.md:709-779, with required apply/restore fixtures at :2241-2261.
The shape is to record an existing routed home into the coordinator as its baseline — an atomic no-replace publication — rather than pretending the home is clean.
Required cases from the review that found this:
routed config, no coordinator: adoption records the existing state, then the lock applies
indeterminate residue: refuses adoption; it cannot seed a row from something it could not classify
invalid or legacy integration record: refuses
existing unversioned or rowless coordinator database: refuses, never adopts
process death partway through adoption: recoverable, not poisoned
Related
Follows the write-substrate unit. src/codex/inject-coordination.ts carries the legacy-uncoordinated boundary this issue removes.
Assessed for implementation this cycle and deliberately not started. Recording why, with
what I checked, so the next attempt does not have to rediscover it.
The gap is real and still exactly as described. decideCoordination in src/codex/inject-coordination.ts returns legacy-uncoordinated for any home that has
routing residue and no coordinator row, and its own comment calls that a temporary boundary
rather than a design. The refusal it routes around is assertInitialStateCanBeCreated in src/codex/transition-state.ts, which is correct to
refuse: writing {0, null} over routed bytes erases the only evidence that an interrupted
transition still needs salvage.
What is actually missing is larger than an adoption branch. The design is fully
specified — devlog/_fin/260804_codex_write_substrate/005_contract.md describes the WP10
compatibility-adoption mode in about seventy lines — but grep -r 'adoption-pending' src/
returns zero matches against 37 in the devlog. None of it exists yet:
a complete-database publisher that builds the whole v1 schema in a unique mode-0600 temp
file and commits the singleton before anything is visible at the final path;
atomic no-clobber publication — same-directory exclusive hard link or a platform
rename-without-replace, where an ordinary replacing rename is forbidden and EEXIST means
another process won;
fsync of the database bytes before publication and of the parent directory after;
adoption-pending accepted by ordinary openers as a ready state rather than legacy-ambiguous, while still not dispatching a history Worker;
the positive-authority gate (sole permitted caller, retained native callback, exact closed
intent, authorizer consumed exactly once) — residue detection or a retry explicitly does
not count as authority.
Why I stopped rather than starting it. This is crash-safety machinery for durable state
in the user's Codex home, where the failure mode of a subtly wrong publication is a corrupted
install rather than a failed test. The contract's own crash boundary — final path atomically
either absent or linked to an already-complete validated database, with no visible
zero-byte interval and no post-publication finally unlink — is the kind of thing that is
either implemented completely with its fixture matrix or not at all. Landing a partial
version inside a wave alongside unrelated fixes would be the worst of both.
It wants its own work-phase, with the apply/restore fixtures the contract already names and
a two-process contention test proving two adoptions cannot both win.
Two corrections to my comment above, from an independent review of it.
The function name was wrong. There is no decideCoordination; it greps to nothing. The
real export is codexWriteCoordinationEligibility in src/codex/inject-coordination.ts.
Everything I described about its behaviour holds — it returns legacy-uncoordinated for
routed homes with no coordinator row, and its own comment calls that a temporary boundary
rather than a design — but in a note whose whole purpose is to save the next person the
rediscovery, a name that greps to nothing is exactly the error that costs them time.
The gap is bigger than I said, which makes the defer more clearly right, not less. The
contract requires that a creator never opens a missing final path with SQLite create:true. src/codex/transition-state.ts does precisely that today, then chmodSyncs afterward. So the
temp-publisher work is not adoption-specific — it rewrites the create path every clean
install already uses. The blast radius is the whole installed base, not the legacy subset.
And a scoping correction worth acting on. I deferred this as one monolith, but there is a
prerequisite subset that stands on its own:
the complete-temp-database publisher plus atomic no-clobber publication, for the ordinary
clean row only — no adoption-pending, no positive-authority gate, no callback plumbing.
That is a hard prerequisite either way, it is independently valuable (it removes the visible
zero-byte / version-0 window on first create for every user today), and it takes the same
kill-boundary fixtures. It does not close this issue. But it means the remaining work is two
reviewable phases — publisher first, adoption second — instead of one large one, which is a
materially better shape to hand to whoever picks this up.
The verdict recorded in devlog/_plan/260822_backlog_disposition_program/060 and 061 still holds, and nothing since has changed the calculus. Coordination covers clean first applies and homes that already carry a valid coordinator; a home routed before the substrate existed keeps its old uncoordinated write path, which is every install predating the substrate.
What keeps this deferred is not effort but safety: adoption has to be correct on a home that may be mid-write by an older binary, and constructing that argument is the actual work here.
Closed on dev by #2612 (squash 82dbb1a3a7f474fbf24105c7a929d1de5ee4d46b).
Routed pre-substrate homes now publish a complete adoption-pending coordinator before any native write, and both apply and restore adopt through the write lock. Indeterminate homes keep the legacy-operable path — that is the case where we genuinely cannot tell what we are looking at, and guessing there is precisely what would strand a home.
Publication is atomic by construction: build in a temp file, commit, fsync, then linkSync into place. Every crash window leaves either no authoritative database or a complete resumable one; there is no state that leaves a half-built coordinator, because a partially written temp file is never the published name.
The crash tests kill a real child process at each of the three checkpoints and then assert the home is still adoptable by resuming a full transition through it. Falsified by replacing linkSync with in-place creation — the obvious simpler implementation — which turns all three red with CodexCoordinatorLegacyAmbiguousError: An existing unversioned coordinator database cannot be adopted automatically. That error is exactly the stranding this issue is about.
GitHub only auto-closes on merges into main; PRs here target dev, so closing manually.
Activity
github-actions commentedon Aug 5, 2026
Automated translation bookkeeping — detected language: unknown.
docs(codex): what leaves this unit, and as what
docs(devlog): fold four audit rounds into the triage unit
Wibias commentedon Aug 8, 2026
Parent tracking issue: #1048
This issue covers legacy-home adoption into the write coordinator.
lidge-jun commentedon Aug 16, 2026
Linking the acceptance boundary for this umbrella, per the Wave 0 triage pass.
#1049 stays the umbrella, but #1798 and #1802 remain independent acceptance cases and are not closed by closing this issue:
ocx stop/restore leaves an unmarkedopenai_base_urland a proxymodels_cachebehind when the Codex app rewrote config after injection. Acceptance: a stop/restore round trip on a config the app rewrote leaves no ocx residue.ocx syncoverwrites a hand-editedconfig.jsonfrom stale server memory. The 2.21.0 save path fixed the save side; the sync path is a separate write-ordering question. Acceptance: an external hand edit is not clobbered by a later sync from an older in-memory snapshot.Each has a different terminal condition, so please close them separately even after coordinator adoption lands.
docs(devlog): record the Wave 3/4 second pass, audit corrections and …
lidge-jun commentedon Aug 18, 2026
Assessed for implementation this cycle and deliberately not started. Recording why, with
what I checked, so the next attempt does not have to rediscover it.
The gap is real and still exactly as described.
decideCoordinationinsrc/codex/inject-coordination.tsreturnslegacy-uncoordinatedfor any home that hasrouting residue and no coordinator row, and its own comment calls that a temporary boundary
rather than a design. The refusal it routes around is
assertInitialStateCanBeCreatedinsrc/codex/transition-state.ts, which is correct torefuse: writing
{0, null}over routed bytes erases the only evidence that an interruptedtransition still needs salvage.
What is actually missing is larger than an adoption branch. The design is fully
specified —
devlog/_fin/260804_codex_write_substrate/005_contract.mddescribes the WP10compatibility-adoption mode in about seventy lines — but
grep -r 'adoption-pending' src/returns zero matches against 37 in the devlog. None of it exists yet:
0600tempfile and commits the singleton before anything is visible at the final path;
rename-without-replace, where an ordinary replacing rename is forbidden and
EEXISTmeansanother process won;
adoption-pendingaccepted by ordinary openers as a ready state rather thanlegacy-ambiguous, while still not dispatching a history Worker;intent, authorizer consumed exactly once) — residue detection or a retry explicitly does
not count as authority.
Why I stopped rather than starting it. This is crash-safety machinery for durable state
in the user's Codex home, where the failure mode of a subtly wrong publication is a corrupted
install rather than a failed test. The contract's own crash boundary — final path atomically
either absent or linked to an already-complete validated database, with no visible
zero-byte interval and no post-publication
finallyunlink — is the kind of thing that iseither implemented completely with its fixture matrix or not at all. Landing a partial
version inside a wave alongside unrelated fixes would be the worst of both.
It wants its own work-phase, with the apply/restore fixtures the contract already names and
a two-process contention test proving two adoptions cannot both win.
Staying open and unassigned.
lidge-jun commentedon Aug 18, 2026
Two corrections to my comment above, from an independent review of it.
The function name was wrong. There is no
decideCoordination; it greps to nothing. Thereal export is
codexWriteCoordinationEligibilityinsrc/codex/inject-coordination.ts.Everything I described about its behaviour holds — it returns
legacy-uncoordinatedforrouted homes with no coordinator row, and its own comment calls that a temporary boundary
rather than a design — but in a note whose whole purpose is to save the next person the
rediscovery, a name that greps to nothing is exactly the error that costs them time.
The gap is bigger than I said, which makes the defer more clearly right, not less. The
contract requires that a creator never opens a missing final path with SQLite
create:true.src/codex/transition-state.tsdoes precisely that today, thenchmodSyncs afterward. So thetemp-publisher work is not adoption-specific — it rewrites the create path every clean
install already uses. The blast radius is the whole installed base, not the legacy subset.
And a scoping correction worth acting on. I deferred this as one monolith, but there is a
prerequisite subset that stands on its own:
That is a hard prerequisite either way, it is independently valuable (it removes the visible
zero-byte / version-0 window on first create for every user today), and it takes the same
kill-boundary fixtures. It does not close this issue. But it means the remaining work is two
reviewable phases — publisher first, adoption second — instead of one large one, which is a
materially better shape to hand to whoever picks this up.
Merge pull request #1943 from lidge-jun/codex/wave5-wp5-triage
docs(devlog): record the WP5 outcome for lidge-jun#1849 and lidge-jun…
docs(devlog): fold the WP5 audit corrections
docs(devlog): map where the split work now lives
2 remaining items
Merge pull request #2386 from lidge-jun/codex/wp6-record
lidge-jun commentedon Aug 23, 2026
Backlog review 2026-08-23 — deferral standing, unchanged.
The verdict recorded in
devlog/_plan/260822_backlog_disposition_program/060and061still holds, and nothing since has changed the calculus. Coordination covers clean first applies and homes that already carry a valid coordinator; a home routed before the substrate existed keeps its old uncoordinated write path, which is every install predating the substrate.What keeps this deferred is not effort but safety: adoption has to be correct on a home that may be mid-write by an older binary, and constructing that argument is the actual work here.
lidge-jun commentedon Aug 25, 2026
Closed on
devby #2612 (squash82dbb1a3a7f474fbf24105c7a929d1de5ee4d46b).Routed pre-substrate homes now publish a complete
adoption-pendingcoordinator before any native write, and both apply and restore adopt through the write lock. Indeterminate homes keep the legacy-operable path — that is the case where we genuinely cannot tell what we are looking at, and guessing there is precisely what would strand a home.Publication is atomic by construction: build in a temp file, commit, fsync, then
linkSyncinto place. Every crash window leaves either no authoritative database or a complete resumable one; there is no state that leaves a half-built coordinator, because a partially written temp file is never the published name.The crash tests kill a real child process at each of the three checkpoints and then assert the home is still adoptable by resuming a full transition through it. Falsified by replacing
linkSyncwith in-place creation — the obvious simpler implementation — which turns all three red withCodexCoordinatorLegacyAmbiguousError: An existing unversioned coordinator database cannot be adopted automatically. That error is exactly the stranding this issue is about.GitHub only auto-closes on merges into
main; PRs here targetdev, so closing manually.Merge from upstream (#8)
docs(codex): what leaves this unit, and as what
docs(devlog): fold four audit rounds into the triage unit
docs(devlog): record the Wave 3/4 second pass, audit corrections and …
docs(devlog): record the WP5 outcome for lidge-jun#1849 and lidge-jun…
docs(devlog): fold the WP5 audit corrections
docs(devlog): map where the split work now lives
Merge pull request lidge-jun#1943 from lidge-jun/codex/wave5-wp5-triage
devlog: backlog disposition program roadmap (work-phase 0, docs-only)
devlog: WP6 — verify and record the lidge-jun#1049 deferral
Merge pull request lidge-jun#2386 from lidge-jun/codex/wp6-record