Skip to content

Resolve CLI callers under Codex's shared daemon - #839

Merged
onevcat merged 2 commits into
mainfrom
fix/codex-daemon-caller-identity
Sep 29, 2026
Merged

onevcat merged 2 commits into
mainfrom
fix/codex-daemon-caller-identity

Conversation

@onevcat

@onevcat onevcat commented Sep 28, 2026

Copy link
Copy Markdown
Owner

Problem

Codex 0.157+ attaches a hand-typed codex TUI to a shared codex app-server --managed-daemon. The daemon runs every thread's shell commands:

  • A command's environment is the daemon's. The daemon copies the environment of whatever last started it: the first TUI, a daemon restart caller, or the auto-update process, which can live for days. Upstream documents this ("per-client environment isolation is not provided") and declined to forward client environments (TUI: allow opt-in client environment metadata on app-server turns openai/codex#44774).
  • The daemon keeps the TUI that started it as its parent. While that TUI lives, a command from any pane walks up to the starter pane's shell.

Effect in Prowl for a hand-typed Codex (Profile launches are not affected, because -c notify=… forces the embedded server):

Surface Before
agents signal, agents dispatch-complete, workflow run / read / deliver / status attributed to the starter pane, or SOURCE_REQUIRED after it exits
$PROWL_PANE_ID used by the prowl-cli skill another live pane, or a closed one

Reproduced on the installed release build: pane B's Codex ran prowl agents signal turn-ended, and Prowl recorded it on pane A (the starter) with binding current.

Change

  • Stop the caller ancestry walk at a Codex managed daemon, so it can never reach the starter pane.
  • At accept time, read the caller's CODEX_THREAD_ID from its KERN_PROCARGS2 environment (server-side, not client-claimed).
  • Every pane sets CODEX_TUI_RECORD_SESSION=1 and a pane-owned CODEX_TUI_SESSION_LOG_PATH. Each submit there carries a client_user_message_id, which the daemon writes as the user item's client_id in the target rollout. CodexDaemonThreadMapper indexes the daemon's open rollouts incrementally, off the main actor, and maps the caller's thread (through subagent lineage) to the pane whose newest indexed submit is in that thread. No evidence means no pane, never a guess.
  • One caller resolution (CallerPaneResolver.pane(for:paneByShellPID:)) now serves signal, dispatch-complete, native hook, workflow, and list. A mapped caller binds a signal as current only when the session log belongs to the pane's current Codex process.
  • prowl list --json reports caller (server-resolved pane and worktree). The prowl-cli skill and docs/components/cli.md use it when CODEX_THREAD_ID is set and keep PROWL_PANE_ID for everything else.
  • Session logs hold prompt text: the directory is 0700, Codex writes files 0600, and Prowl deletes a pane's log when its surface is freed and sweeps logs older than seven days at launch.
  • Design and evidence: docs-ai/073-codex-daemon-caller-identity/.

Verification

  • New unit tests cover environment parsing, the ancestry cut (starter-pane regression), the mapper (shared cwd, /new, pending steer, subagent lineage, two panes on one thread, missing evidence, incremental file reads), the list caller field, pane environment, and log sweeping.
  • make check, make test (3594 passed, 0 failed in supacode-tests), make build-cli, make test-cli-smoke, make test-cli-unit, make test-cli-integration, and make build-app pass.
  • Live acceptance in a Debug instance on a dedicated socket, with an isolated CODEX_HOME daemon started by pane X's Codex (so its parent chain reaches X's shell):
Scenario Result
Pane Z's Codex before any submit: !prowl list caller absent (not X)
Pane Y's Codex: $PROWL_PANE_ID X's id (inherited)
Pane Y's Codex: prowl list caller and agents signal Y, binding current
External agents dispatch to Y; Codex runs dispatch-complete receipt completed / succeeded on Y
Y's Codex runs workflow run prowl.handoff --input next=save, read, token deliver run completed, current role Y
Close Y's tab session log removed after the undo-close window

Follow-up

Daemon-mode Codex log-based working/idle detection will reuse CodexDaemonThreadMapper in a stacked PR.

Codex 0.157+ runs the commands of a hand-typed `codex` in a shared
app-server daemon. The daemon keeps the environment of the process that
last started it, and keeps that process as its parent. So a command
from any pane walked up to the starter pane's shell and was attributed
to the wrong pane, or failed with SOURCE_REQUIRED after the starter
exited. `PROWL_PANE_ID` inside Codex names the same wrong pane.

- Stop the caller ancestry walk at a Codex managed daemon.
- Read the caller's CODEX_THREAD_ID from its KERN_PROCARGS2 environment
  at accept time.
- Every pane records a Codex TUI session log. Map the thread to the
  pane whose newest indexed submit (client_user_message_id) is in that
  thread's root rollout. The mapping runs off the main actor.
- Use one caller resolution for signal, dispatch-complete, native hook,
  workflow, and list. A mapped caller binds a signal when the session
  log belongs to the pane's current Codex process.
- `prowl list --json` reports the resolved `caller`. The prowl-cli skill
  and the CLI manual use it inside Codex.
- Record the design in docs-ai 073.
@onevcat
onevcat merged commit 5666695 into main Sep 29, 2026
1 check passed
@onevcat
onevcat deleted the fix/codex-daemon-caller-identity branch September 29, 2026 10:57
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