Skip to content

codex: capture the conversation pin in a shared folder, so a restart can resume it - #36

Closed
lvwerra wants to merge 1 commit into
mainfrom
fix/codex-pin-shared-folder
Closed

codex: capture the conversation pin in a shared folder, so a restart can resume it#36
lvwerra wants to merge 1 commit into
mainfrom
fix/codex-pin-shared-folder

Conversation

@lvwerra

@lvwerra lvwerra commented Aug 5, 2026

Copy link
Copy Markdown
Member

The reported symptom

A codex session was stopped and restarted from the UI and came back empty — no conversation, no history — after ~3h of work in /data/workspaces/agent-manager.

Nothing was lost on disk. The rollout was intact the whole time: 019fd29a-97e5-7d11-b36e-3ce290b0a0c1, 277 lines, 1.06MB, payload.cwd exactly the session's workdir, thread_source: user. The manager simply had no record of it.

What it actually was

am-image-inputs-aee9c3 had no codexSessionId and no codexRollout, three hours in. The watcher's tick checked the shared-folder guard first and skipped capture entirely:

if (folderIsShared(session.id, workdir, 'codex')) {
  if (!warnedShared) { /* warn once */ }
} else {
  tryCaptureCodexId(session.id, workdir, since);   // ← never reached
}

folderIsShared was true for the life of the pane: agent-manager-5-b0fa07 (codex) was live in the same folder. So the pin was never captured — not the initial one, not any. The server said so itself, twice:

[codex] am-image-inputs-aee9c3: folder shared with another live session — not following thread resets here
[codex] agent-manager-5-b0fa07: folder shared with another live session — not following thread resets here

Unpinned, resumeCmd skips the pinned branch and reaches the generic codex one, which correctly refuses resume --last in a shared folder — --last scopes to the cwd, so it could resume a sibling's thread — and runs bare exec codex. An empty pane was the only possible outcome. agent-manager-2-93de86, same folder, is pinned: it launched while it was the only codex there.

Ruled out along the way: CODEX_HOME in the server's own env (set correctly, /home/node/local/codex-home); the sessions symlink onto the bucket breaking the walk (readdirSync resolves it, 2026/ is a real dir, depth 3 of 5); the rollout's own metadata failing a gate (cwd matched, born 3.5s after launch, thread_source: user, not a subagent); FUSE mtime hiding the file from codexRolloutsSince.

Why the guard was there, and what it should have covered

The guard is right about reset-following. A new conversation appearing in a shared folder is genuinely unattributable — our /clear, or a sibling's? — and claiming one would take a live agent's thread away from it. That is the same problem #23 solved for claude with a breadcrumb, and #35 is what finally makes that breadcrumb work. Codex has no breadcrumb, so it can only refuse.

The initial capture has no such ambiguity, and tryCaptureCodexId already carries the checks that make it safe in a shared folder: the claimed set (never take a rollout another session pinned), the cwd match, born-after-this-launch, and the subagent filter. Those are precisely the "don't take someone else's thread" checks. Skipping the whole call threw them away along with the bug they prevent.

Three changes

1. codexCandidate(), split out of tryCaptureCodexId

Mirrors claudeCandidate: all the gates, no writes, returns { id, p } or null. Exported, so the gates are directly testable — they never were before.

2. A bornBefore cap

codexCandidate takes an upper bound on how late a conversation may have been born and still count as ours. Used only in the shared case, set to launch + 2 min. Codex writes its session_meta line as the TUI starts (3.5s after launch in this session), so it is ~20x the margin needed, and a sibling's reset minutes or hours in stays out of reach — the one hazard that made capture-in-a-shared-folder look unsafe.

3. codexCaptureMode() — the policy, as a pure function

mode when behaviour
follow folder is ours capture, keep following resets — unchanged
window shared, unpinned capture, bounded to the launch window — this case used to do nothing
pinned shared, pinned don't follow resets — unchanged, still warns once
expired shared, unpinned, window gone nothing safe left; warns once

expired is new behaviour worth naming: a pane that genuinely never produced a rollout now says so once, instead of silently restarting empty like this one did.

What this does not fix

Two codex panes launching in the same folder within ~15s of each other are still ambiguous — that is the existing born-after-launch slack, and the sibling's rollout is only excluded by it. Back-to-back restarts further apart are fine: each pane's window opens at its own launch and the earlier rollout falls outside it, plus whoever pins first claims it. A breadcrumb-style trust signal for codex, the equivalent of #23/#35 for claude, is the real fix for that and is not attempted here.

Also unchanged: resumeCmd's shared-folder check counts stopped siblings too. That is correct — resume --last doesn't care whether the sibling is running, its rollout is still the most recent in the cwd — and it matters much less now, since the pinned branch returns first.

Testing

server/test/repin.test.mjs34 checks, up from 21. Full npm test green (5 suites, 50 checks).

  • 7 new checks on codexCandidate against real rollout files in a temp CODEX_HOME: own conversation captured; another folder ignored; a pre-launch rollout rejected despite a fresh mtime; a subagent rollout skipped; a later conversation followed when the folder is ours but out of reach when shared; a rollout another session pinned left alone.
  • 6 new checks on codexCaptureMode, including both sides of the window boundary (SINCE + 120_000window, +120_001expired).
  • Verified the new checks have teeth: dropping the bornBefore line fails 2 of them.

Not verified end-to-end on the live Space: that a folder-sharing codex pane now restarts onto its own conversation needs this deployed — the running container still skips capture, which is why the log evidence above shows only the refusal.

🤖 Generated with Claude Code

A codex pane sharing its folder with another live codex session never got a
conversation pin at all: the watcher tick checked folderIsShared FIRST and
skipped tryCaptureCodexId entirely, so codexSessionId/codexRollout stayed
empty for the life of the pane. resumeCmd then correctly refuses
`resume --last` in a shared folder and runs bare `codex`, so every restart
came back empty with the conversation still on disk.

Observed live: am-image-inputs-aee9c3 ran ~3h in /data/workspaces/agent-manager
alongside agent-manager-5-b0fa07, logged

  [codex] am-image-inputs-aee9c3: folder shared with another live session
          — not following thread resets here

twice, and restarted into an empty pane while its 277-line rollout
(019fd29a-97e5-7d11-b36e-3ce290b0a0c1) sat unreferenced.

The guard was written for reset-following, where a new conversation in a
shared folder is genuinely unattributable — our /clear, or a sibling's? The
INITIAL capture has no such ambiguity, and tryCaptureCodexId already carries
the checks that make it safe: the claimed set, the cwd match, born-after-launch,
and the subagent filter. Split the two: capture always runs, reset-following
still stops at a shared folder.

- codexCandidate() is split out of tryCaptureCodexId (mirrors claudeCandidate)
  and takes a `bornBefore` cap, so a sibling's LATER conversation stays out of
  reach while our own launch-window one is claimable.
- codexCaptureMode() is the pure policy: follow / window / pinned / expired.
- 'expired' logs once, so a pane that really found no rollout says so instead
  of restarting empty in silence.

Codex launches within ~15s of each other in one folder remain ambiguous — the
existing born-after-launch slack — and unlike claude (#23, #35) codex has no
breadcrumb to disambiguate. Noted, not fixed here.

server/test/repin.test.mjs: 34 checks, up from 21. Full npm test green (5
suites, 50 checks). Verified the new checks have teeth — dropping the
bornBefore cap fails 2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant