Skip to content

Adopt pre-substrate Codex homes into the write coordinator #1049

Closed

Description

@lidge-jun

The gap

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.

Activity

  1. github-actions commented on Aug 5, 2026

    @github-actions
    Contributor

    Automated translation bookkeeping — detected language: unknown.

  2. Wibias commented on Aug 8, 2026

    @Wibias
    Contributor

    Parent tracking issue: #1048

    This issue covers legacy-home adoption into the write coordinator.

  3. added
    cliCLI, config inject, packaging flags
    on Aug 15, 2026
  4. lidge-jun commented on Aug 16, 2026

    @lidge-jun
    OwnerAuthor

    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:

    Each has a different terminal condition, so please close them separately even after coordinator adoption lands.

  5. lidge-jun commented on Aug 18, 2026

    @lidge-jun
    OwnerAuthor

    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.

    Staying open and unassigned.

  6. lidge-jun commented on Aug 18, 2026

    @lidge-jun
    OwnerAuthor

    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.

  7. added a commit that references this issue on Aug 18, 2026
  8. 2 remaining items

  9. added a commit that references this issue on Aug 22, 2026
  10. lidge-jun commented on Aug 23, 2026

    @lidge-jun
    OwnerAuthor

    Backlog review 2026-08-23 — deferral standing, unchanged.

    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.

  11. lidge-jun commented on Aug 25, 2026

    @lidge-jun
    OwnerAuthor

    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.

  12. added a commit that references this issue on Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcliCLI, config inject, packaging flags

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions