Repository navigation
fix(link): a refused Child disconnect rolls back, Codex-edited config.toml no longer blocks it, desktop refusals name their cause #8530
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
lidge-jun
wants to merge
16
commits into
dev
Choose a base branch
from
fix/child-disconnect-followups
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+960
−30
Open
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
3b2017d
fix(link): ocx restart recycles a client runtime whose connection was…
3bca971
fix(link): name the failed condition and its fix in the Child restart…
69867af
fix(link): a client injection takes over the Codex journal so a super…
d79b051
fix(cli): point refusal and status hints at commands that work beside…
73464c7
fix(cli): explain client_catalog_ownership_changed on disconnect and …
9c20cde
docs(link): Child leave and re-link behavior under the desktop app or…
95b4a51
fix(cli): name the desktop-remote folder that blocks disconnect and h…
37f295d
fix(link): a refused disconnect rolls back, Codex-edited config.toml …
216dda7
docs(link): disconnect rollback, Codex-edited config and named deskto…
0fd13d7
fix(link): review round 1 - one disconnect at a time, keep a receipt …
6bd12c3
fix(link): final review - quote only allowlisted keys and fixed table…
ae70689
fix(link): put back the person's own web_search line OpenCodex set as…
lidge-jun 6255df6
fix(link): keep each line's own ending when putting the web_search li…
lidge-jun 6881256
test(link): byte-for-byte boundaries and ownership for putting the we…
lidge-jun 0da9a78
Merge origin/dev (with #8529 squashed as de0914a1a) into fix/child-di…
lidge-jun 90cad14
fix(link): wait for config-directory ACL admission before taking the …
lidge-jun File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
14 changes: 14 additions & 0 deletions
14
devlog/_plan/261111_child_disconnect_followups/000_plan.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| # Child disconnect follow-ups (O7, O8, O9, O10) | ||
|
|
||
| Stacked on #8529 (`fix/child-restart-desktop-owned`). Field reports: six Remote Link Children moved to new SSH aliases on 2026-11-11, OpenCodex 2.100.2. | ||
|
|
||
| | Unit | Doc | Kind | Outcome wanted | | ||
| |---|---|---|---| | ||
| | O8 | 010 | defect, test-first | a refused disconnect never leaves state that stops the runtime from starting; a retry finishes or rolls back from `prepared`, including a receipt already stuck on 2.100.2 | | ||
| | O9 | 020 | defect, test-first | disconnect succeeds when Codex edited config.toml after injection and no OpenCodex line remains; never overwrites Codex or user lines; keeps the journal while an OpenCodex line remains | | ||
| | O7 | 030 | message, test-first | each `desktop_lifecycle_unsafe` refusal names the path or condition and the fix, code at the end | | ||
| | O10 | 040 | research + proposal only | what a safe "restart Codex clients" is and where it belongs | | ||
|
|
||
| Order: O8 and O9 share `disconnectClient` and land together (O9 makes the O8 retry succeed); O7 next; O10 is a written proposal in the PR body. | ||
|
|
||
| Verification floor: each new test fails on the base branch first; `bun run typecheck`, `bun run structure:check`, `bun run privacy:scan`, the layout guards, and the files that import the changed modules. |
18 changes: 18 additions & 0 deletions
18
devlog/_plan/261111_child_disconnect_followups/010_o8_prepared_receipt_rollback.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| # O8 — a refused disconnect rolls back its `prepared` receipt | ||
|
|
||
| ## Cause | ||
|
|
||
| `disconnectClient` (src/client/connect.ts) writes `desktop-remote/disconnect.json` with phase `prepared`, then runs `restoreConnectedCodex` outside the lifecycle lock. A throw there leaves the receipt. Every non-`complete` receipt makes `observeClientRotationRecovery` (src/client/state.ts) answer `client_disconnect_pending`, and client startup refuses (src/cli/index.ts). A retry repeats the same Codex restore and fails the same way. | ||
|
|
||
| ## Decision | ||
|
|
||
| `desktop_restored` is the commit point. Before it, the receipt records intent only: the Claude Desktop store, the catalog, the service token and the connection are untouched. So a failure before `desktop_restored` aborts: the receipt is removed (compare-and-delete against the receipt this run read), and the `desktop-remote` folder is removed too when the receipt was the only thing in it. At or after `desktop_restored` a retry must finish, as today. | ||
|
|
||
| What a partial Codex journal restore leaves behind: the journal (recovery evidence), config.toml as it was, possibly the generated profile already reverted. The connection stays `connected`; the client runtime's next sync re-injects routing and regenerates the profile. | ||
|
|
||
| The startup refusal names its fix: run `ocx disconnect` again (same `--keep-catalog` choice) to finish or roll back. | ||
|
|
||
| ## Tests (fail on base first) | ||
|
|
||
| - refused disconnect: receipt gone, folder gone, connection connected, rotation gate `clean` (startup would pass), journal kept. | ||
| - upgrade path: a `prepared` receipt written as 2.100.2 left it; `ocx disconnect` on the new build either completes (Codex side now restorable) or rolls back (still refused), never needs a folder moved. |
21 changes: 21 additions & 0 deletions
21
devlog/_plan/261111_child_disconnect_followups/020_o9_codex_edited_config.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| # O9 — disconnect after Codex edited config.toml | ||
|
|
||
| ## Cause | ||
|
|
||
| `compareJournalState` (src/codex/journal.ts) accepts config.toml only when it equals the original snapshot or the first injected bytes (hash). Codex appends `[projects."…"] trust_level` blocks and notice flags; the restore is then partial forever. | ||
|
|
||
| ## Rule | ||
|
|
||
| Inside one held Codex config write lock, after `restoreJournalState` reports incomplete: | ||
|
|
||
| - proceed only if the journal is verified, the profile is restored, there is no lock or ownership refusal, and config.toml is what changed; | ||
| - and the owned-field strip that `ocx restore` uses (`stripOpencodexConfigResult` with the journaled values) would change nothing — no marked or journaled base URL / realtime URL, no `opencodex` provider or table, no managed web_search or subagent defaults, no profile section. | ||
|
|
||
| Then config.toml is left byte-for-byte and the journal is removed: its only remaining use, a whole-file replay of the pre-injection snapshot, would delete the lines Codex wrote, so it is never safe to apply again. If any OpenCodex line remains, the journal is kept and the refusal names the file and the remaining OpenCodex settings, quoting only an allowlist of keys OpenCodex writes and fixed table labels (never values, continuation lines, or quoted table names), with the fix: remove each named key with its whole value and each named table with every line under it, then run `ocx disconnect` again. `ocx restore` is not named: on a connected Child it also stores Codex integration as off, and nothing here proves a later link turns it back on. | ||
|
|
||
| Not done here: stripping the remaining OpenCodex lines inside disconnect. `ocx restore`'s strip does that with history-provider handling, and folding it into disconnect is a follow-up decision. | ||
|
|
||
| ## Tests | ||
|
|
||
| - positive: Codex-appended lines, no routing → disconnect completes, config.toml unchanged, journal gone. | ||
| - negative: Codex-appended lines plus a remaining OpenCodex line (journaled `openai_base_url` without its marker) → refused, config.toml unchanged, journal kept, message names `openai_base_url`, config.toml and the fix. |
7 changes: 7 additions & 0 deletions
7
devlog/_plan/261111_child_disconnect_followups/030_o7_named_desktop_refusals.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # O7 (rest) — name every desktop_lifecycle_unsafe cause | ||
|
|
||
| `inspectRemoteDesktopStore` (src/claude/desktop-remote-store.ts) folds its failure sites into `conflict` / `unsafe`. Thread a reason and path out of each site class and map each to a message with the condition, the path and the fix, ending `(code desktop_lifecycle_unsafe)`. | ||
|
|
||
| Classes (to be confirmed against source): store owned by another connection; OpenCodex profile removed or renamed in Claude Desktop; OpenCodex profile edited in Claude Desktop; an unreadable or unsafe store / library file; the folder with no state or receipt (#8529). | ||
|
|
||
| Tests: one per class through `disconnectClient`, asserting path, fix and trailing code. |
19 changes: 19 additions & 0 deletions
19
devlog/_plan/261111_child_disconnect_followups/040_o10_restart_codex_clients.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| # O10 — restarting running Codex clients after sync (proposal, not built) | ||
|
|
||
| ## What happens today (source-checked) | ||
|
|
||
| - `ocx sync` on a Child takes the connected path (`src/cli/sync-command.ts` → `syncConnectedClient`). After it, `handleConnectedSyncCatalogWrite` runs the restart scope only when that sync wrote the catalog or the models cache (`if (!result.catalogWritten && !result.cacheSynced) return`). | ||
| - Without a restart flag, the same write path calls `afterCatalogWriteHandleAppServers({ restart: false })` (`src/codex/app-server-processes.ts`), which prints "WARNING: N Codex app-server process(es) still running … Re-run with `ocx sync --restart-codex`" for app-servers started before the catalog's mtime. `ocx status` reports `clients=pending-restart(pid …)` from the same stale-client observation (`src/codex/autostart-health.ts`). | ||
| - Why the advice did not apply: the re-run writes nothing (the catalog is already current), so the restart scope never runs. The registry already says so for `ocx system codex-restart`: "`sync --restart-codex` is not a substitute: it restarts only as a side effect after a catalog, cache, or changed configuration write". | ||
| - `ocx system codex-restart --yes` restarts on request, but it posts to `/api/system/codex-restart` on the running runtime's management API. A Child's client runtime serves only the machine listener (`src/client/machine-listener.ts`: `/healthz`, the bound `POST /api/system/restart`); nothing under `src/client/` serves `codex-restart`, so on a Child this command has no endpoint to reach (to confirm with a test before relying on it). | ||
| - The Home's `ocx link restart-codex` and `ocx link sync --restart-*` send a second pinned `ocx sync --expect-link <id> --json --restart-codex` to the Child (`src/link/ssh-argv.ts` `remotePinnedSyncArgv`). That goes through the same write-gated path, so a second sync that writes nothing restarts nothing. That is likely the same gap and needs a test to confirm. The Home withholds the restart while a relayed model request for that Child is in flight (`src/link/sync-then-restart.ts`, `restart-skipped:link-active`). | ||
| - Idleness: OpenCodex sees a turn only as a request through its own proxy (`getActiveTurnCount()` in `src/server/lifecycle.ts`, used by `src/codex/catalog-self-heal.ts`; the link's in-flight count on the Home). It cannot see a Codex app-server's own state: a turn waiting on an approval prompt, or a composer draft, has no request in flight. | ||
|
|
||
| ## Proposal | ||
|
|
||
| 1. **Restart what is actually stale, on request.** When `ocx sync --restart-codex` / `--restart-app-server-only` writes nothing, still restart the app-servers that `computeCodexAppServerCatalogStatus` classifies as stale (started at or before the current catalog's mtime). That makes the command the warning prints do what it says, and makes the Home's second pinned sync effective. Processes are the ones `listCodexAppServerProcesses` already selects (same user, Codex app-server / code-mode-host argv), narrowed to the stale set. A fresh process is never touched. The desktop app is quit only for the full `--restart-codex` scope, as today. | ||
| 2. **Idle gate.** Before restarting, the Child checks its own client runtime for active turns, which needs a small `activeTurns` field on the client `/healthz`. If a turn is active it withholds the restart and reports `restart-skipped:active`, matching the Home's `link-active`. Because app-server-internal state stays invisible, the explicit flag (and `--yes` for the desktop scope) remains the consent. "Idle" here means no proxied request in flight, not "safe to discard UI state". | ||
| 3. **Where it belongs.** Child-local: `ocx sync --restart-*` (the code path above), so a person on the Child, the Home's pinned sync, and `ocx link restart-codex` all share one implementation. `ocx system codex-restart` on a Child should either run the same local module or refuse with a message naming `ocx sync --restart-app-server-only`, instead of reaching for a management API the client runtime does not serve. | ||
| 4. **Hints.** `clients=pending-restart` in `ocx status` and the sync warning on a Child should name the command that works there. | ||
|
|
||
| Not built in this PR: (1) changes Home fan-out restart behaviour and needs its own tests across the three entry points. (2) adds a field to the client health contract. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update the translated Child-disconnect recovery instructions.
These lines add the retry command for a pending receipt and the steps for removing remaining OpenCodex settings. The corresponding Child-removal sections in the ja, ko, ru, and zh-cn guides contain neither recovery procedure. Readers of those guides cannot find the new startup-recovery steps there. Update those sections from the English canonical guide. (raw.githubusercontent.com)
As per coding guidelines, “Update all directly affected pages when a user workflow changes.”
Source: Coding guidelines