What happened?
Two Codex sessions worked in one repository at the same time: A in the main checkout, B in a linked worktree (codex --worktree). When A committed, B was guest-condensed into the checkpoint of A's commit, although B had not touched any of the committed files.
Expected: the checkpoint of A's commit contains only A's session.
This happened six times in one day in one repository: five commits picked up the same session from a ~/.codex/worktrees/<id>/<repo> worktree, and one picked up another.
No transcript is lost, but B's conversation up to that moment is attributed to a commit it had nothing to do with. The guest condensation also consumes B's checkpoint window, so B's own next checkpoint starts mid-conversation.
Steps to reproduce
- Codex CLI 0.157 or later with the default
daemon_auto_start, and entire enable with the Codex integration in a repository.
- Start Codex in the main checkout (session A) and give it a task that ends with a commit.
- After A's turn has started, start Codex in a linked worktree of the same repository (
codex --worktree, session B) and give it a long task.
- While B's turn is still running, let A commit.
- The checkpoint of A's commit contains both sessions, and
.entire/logs/entire.log shows session guest-condensed from a sibling worktree for B.
These steps follow the code path described under Additional context and the incidents above; I have not run them as a controlled reproduction.
Entire CLI version
Entire CLI 0.11.3 (go1.27.1, linux/amd64). None of the files involved has changed on main since (2dbea8b).
OS and architecture
Debian GNU/Linux 12 (bookworm) container, Linux 6.12 x86_64
Agent
Codex CLI 0.158.0; the TUI sessions run inside codex app-server --managed-daemon
Terminal
herdr over SSH
Logs / debug output
From the post-commit hook of A's commit, with timestamps dropped and IDs and paths replaced:
{"level":"INFO","msg":"attribution calculated","session_id":"<B>","component":"attribution","agent_lines":14,"human_added":0,"human_modified":0,"human_removed":0,"total_committed":14,"agent_percentage":100,"accumulated_user_added":0,"accumulated_user_removed":0,"files_touched":12}
{"level":"INFO","msg":"session condensed","component":"checkpoint","strategy":"manual-commit","session_id":"<A>","checkpoint_id":"<CP>","checkpoints_condensed":1,"transcript_lines":308}
{"level":"INFO","msg":"session guest-condensed from a sibling worktree; shadow state untouched","component":"checkpoint","strategy":"manual-commit","session_id":"<B>","checkpoint_id":"<CP>","home_worktree":"~/.codex/worktrees/<id>/<repo>"}
The first line carries A's attribution, as recorded in A's checkpoint metadata, under B's session_id. B's session state records "owner": {"pid": <daemon pid>, "name": "codex"}, the PID of codex app-server --listen unix:// --managed-daemon.
Additional context
Root cause. Since Codex 0.157 the TUI hosts its sessions in one shared background process, codex app-server --managed-daemon (automatic start is on by default since openai/codex#47179). Entire records a session's owner as the nearest ancestor of the hook process that is not a shell or Entire itself (ResolveOwner), so every Codex session on the machine gets the same owner: the daemon.
On a commit, findSessionsForCommitLinking adds the session returned by findSessionByCommitAncestry to the worktree-matched sessions. The daemon is an ancestor of every commit a Codex session makes, so all Codex sessions of the repository match at the same depth, and isNearerOwner breaks the tie by the later LastInteractionTime, which moves at turn boundaries. B had started its turn after A's last turn boundary, so B won and was appended as a guest. Being ACTIVE, it was condensed without an overlap check, and the guest branch consumed its checkpoint window.
The recency tie-break is meant for one process hosting sessions one after another, the resume case its comment describes. The Codex daemon hosts them concurrently.
Caller resolution takes the same tie-break: resolveCallerIdentity also ranks with isNearerOwner, so at equal depth recency wins even when the environment names one of the tied sessions. That is consistent with the attribution calculated line above.
Suggested fix. Identify the committing session by the agent-published session variable (agent.CallerSessionCandidates()) whenever one is set. Codex injects CODEX_SESSION_ID into the environment of its shell commands (inject_session_env), and Entire already declares it as Codex's caller-session variable. It names the session whose command is running, which process ancestry can only approximate. I have not verified that the variable reaches the git hook when the session runs in the daemon.
Keep process ancestry for what the variable cannot do: ordering several claims, since a nested agent inherits its parent's variable and the nearest owner is the author, and covering agents that publish no variable. Where nothing but recency separates sessions that share one owner process, linking only the worktree-matched sessions looks safer than attaching one that may be foreign, with a notice like the one resolveWorktreeCandidates prints when it declines to guess between live worktrees. The committing session is still linked whenever it commits in its home worktree. Commit linking and caller resolution could share this rule.
#2531 relies on the same identification (it re-homes "the session that process ancestry identified"), so under a shared daemon it would inherit the misidentification.
Workaround. Start concurrent Codex sessions of one repository with codex --no-daemon, so that each runs in its own process.
Happy to send a PR once the approach is agreed.
What happened?
Two Codex sessions worked in one repository at the same time: A in the main checkout, B in a linked worktree (
codex --worktree). When A committed, B was guest-condensed into the checkpoint of A's commit, although B had not touched any of the committed files.Expected: the checkpoint of A's commit contains only A's session.
This happened six times in one day in one repository: five commits picked up the same session from a
~/.codex/worktrees/<id>/<repo>worktree, and one picked up another.No transcript is lost, but B's conversation up to that moment is attributed to a commit it had nothing to do with. The guest condensation also consumes B's checkpoint window, so B's own next checkpoint starts mid-conversation.
Steps to reproduce
daemon_auto_start, andentire enablewith the Codex integration in a repository.codex --worktree, session B) and give it a long task..entire/logs/entire.logshowssession guest-condensed from a sibling worktreefor B.These steps follow the code path described under Additional context and the incidents above; I have not run them as a controlled reproduction.
Entire CLI version
Entire CLI 0.11.3 (go1.27.1, linux/amd64). None of the files involved has changed on
mainsince (2dbea8b).OS and architecture
Debian GNU/Linux 12 (bookworm) container, Linux 6.12 x86_64
Agent
Codex CLI 0.158.0; the TUI sessions run inside
codex app-server --managed-daemonTerminal
herdr over SSH
Logs / debug output
From the post-commit hook of A's commit, with timestamps dropped and IDs and paths replaced:
The first line carries A's attribution, as recorded in A's checkpoint metadata, under B's
session_id. B's session state records"owner": {"pid": <daemon pid>, "name": "codex"}, the PID ofcodex app-server --listen unix:// --managed-daemon.Additional context
Root cause. Since Codex 0.157 the TUI hosts its sessions in one shared background process,
codex app-server --managed-daemon(automatic start is on by default since openai/codex#47179). Entire records a session's owner as the nearest ancestor of the hook process that is not a shell or Entire itself (ResolveOwner), so every Codex session on the machine gets the same owner: the daemon.On a commit,
findSessionsForCommitLinkingadds the session returned byfindSessionByCommitAncestryto the worktree-matched sessions. The daemon is an ancestor of every commit a Codex session makes, so all Codex sessions of the repository match at the same depth, andisNearerOwnerbreaks the tie by the laterLastInteractionTime, which moves at turn boundaries. B had started its turn after A's last turn boundary, so B won and was appended as a guest. Being ACTIVE, it was condensed without an overlap check, and the guest branch consumed its checkpoint window.The recency tie-break is meant for one process hosting sessions one after another, the resume case its comment describes. The Codex daemon hosts them concurrently.
Caller resolution takes the same tie-break:
resolveCallerIdentityalso ranks withisNearerOwner, so at equal depth recency wins even when the environment names one of the tied sessions. That is consistent with theattribution calculatedline above.Suggested fix. Identify the committing session by the agent-published session variable (
agent.CallerSessionCandidates()) whenever one is set. Codex injectsCODEX_SESSION_IDinto the environment of its shell commands (inject_session_env), and Entire already declares it as Codex's caller-session variable. It names the session whose command is running, which process ancestry can only approximate. I have not verified that the variable reaches the git hook when the session runs in the daemon.Keep process ancestry for what the variable cannot do: ordering several claims, since a nested agent inherits its parent's variable and the nearest owner is the author, and covering agents that publish no variable. Where nothing but recency separates sessions that share one owner process, linking only the worktree-matched sessions looks safer than attaching one that may be foreign, with a notice like the one
resolveWorktreeCandidatesprints when it declines to guess between live worktrees. The committing session is still linked whenever it commits in its home worktree. Commit linking and caller resolution could share this rule.#2531 relies on the same identification (it re-homes "the session that process ancestry identified"), so under a shared daemon it would inherit the misidentification.
Workaround. Start concurrent Codex sessions of one repository with
codex --no-daemon, so that each runs in its own process.Happy to send a PR once the approach is agreed.