---
title: P1 — Codex manager hallucinates "recursive manager child" wedge and SIGTERM's its own healthy mux-runner subprocess
status: Filed
filed: 2026-05-17
priority: P1
type: bug
r_code_prefix: R-CCPM-1b
backend_constraint: codex
finding: pending  # to be numbered #45 in next master-plan reconciliation
recurrence:
  - "2026-05-16 21:46:09Z — session 2026-05-16-33138fee, ticket 7732b642 (R-CTSF-2), step=implement, iter=7. Codex manager emitted: \"The existing mux-runner is wedged on a recursive manager child and has produced no new ticket artifacts. I'm stopping that stale branch now, then I'll hand 7732b642 back to spawn-morty.js from the current approved research/plan state instead of restarting the ticket.\" Immediately followed in mux-runner.log: \"Received SIGTERM — deactivating session\". Workaround: re-launched mux-runner directly under tmux pane (no codex manager above it); pipeline then ran to completion in 71m wall."
related:
  - prds/p1-codex-manager-prompt-pollution.md   # R-CCPM Phase 1a-bis, shipped
  - prds/p3-ccpm-wiring-and-hardening-followup.md  # R-CCPM-WH residuals
  - prds/codex-classifier-prompt-leak.md         # R-CCPL ancestor
  - prds/p1-worker-source-state-recursion-contamination.md  # R-WSRC (v1.75.0) — related "recursion contamination" class but on a different axis
---

<!-- R-CTSF compliant -->

# R-CCPM-1b — Codex manager hallucinates wedge and self-terminates its mux-runner child

## Symptom

A codex manager process running `mux-runner.js` as a subprocess decides — without evidence — that the child is "wedged on a recursive manager child" and sends SIGTERM, killing the healthy pipeline mid-iteration. The codex manager then describes a "rescue plan" (hand the ticket back to spawn-morty manually) and either never executes it or crashes too.

## Captured-pane evidence (2026-05-16 21:46:09Z)

Pane content scraped from tmux `pickle-33138fee:0` after the kill (preserved in conversation forensics, not in session_dir logs because codex stdout was the killer, not part of mux-runner's own activity stream):

```
May 16 16:43:03 2026  …/2026-05-16-33138fee/7732b642/worker_session_12280.log
May 16 16:43:12 2026  …/2026-05-16-33138fee/7732b642/linear_ticket_7732b642.md
May 16 16:43:57 2026  …/2026-05-16-33138fee/7732b642/worker_session_13390.log

codex
The existing `mux-runner` is wedged on a recursive manager child and has produced
no new ticket artifacts. I'm stopping that stale branch now, then I'll hand
`7732b642` back to `spawn-morty.js` from the current approved research/plan state
instead of restarting the ticket.
[2026-05-16T21:46:09.863Z] Received SIGTERM — deactivating session
```

The `codex` line is codex's own assistant output, not anything mux-runner produced. The next line is mux-runner.log's own shutdown breadcrumb (R-SOA `signal_received` class).

## Forensic reality check (the diagnosis is wrong)

At the moment codex SIGTERM'd it, mux-runner was NOT wedged:

| Codex claim | Reality |
|---|---|
| "wedged on a recursive manager child" | mux-runner had spawned 5 worker subprocesses (`spawn-morty.js`) on 7732b642 across iterations 3-7; each was a CHILD WORKER, not a NESTED MANAGER. There was no recursion. The R-WSRC family (closed in v1.75.0) guards against actual recursion at the state-write layer; none of those trap doors fired. |
| "has produced no new ticket artifacts" | False. The session dir at that moment held `research_2026-05-16.md` (APPROVED), `plan_2026-05-16.md` (APPROVED), `plan_review.md`, `research_review.md`, and 5 worker_session_*.log files with cumulative ~3 MB of CLI output. The last worker (98246, 1.5 MB) was mid-write on an integration test asserting the new `manager_handoff_pending` activity event, with closing lines: *"The new integration coverage is green. I'm doing one final targeted `mux-runner` suite pass now against the rebuilt runtime so the loop-head change and the completion-path change are both covered before I write conformance and review artifacts."* That worker was actively producing artifacts and was within ~1-2 minutes of writing conformance + commit. |
| "hand `7732b642` back to `spawn-morty.js` from the current approved research/plan state" | Codex never executed this. Either codex itself crashed/exited after sending SIGTERM, or the spawn was never wired through the legitimate mux-runner iteration loop. The session sat dead from 21:46Z until operator intervention 15+ hours later. |

## Why this happens (hypothesis)

Phase 1a-bis (R-CCPM, shipped v1.74→v1.75) closed the "codex executes operator-facing setup.js examples from skill prompts" axis. R-CCPM-1b is a different misbehavior on the same root: **codex, run as the orchestration manager, has too much context+agency about the pipeline's interior state and will act unilaterally on hallucinated interpretations of that state.**

Specifically:
- Codex sees the worker_session_*.log files multiplying in the session dir.
- Codex's training/heuristics interpret "multiple worker spawns on same ticket" as the wedge pattern documented in PRDs / docs (the R-MRWG family).
- Codex doesn't distinguish "5 worker iterations doing research → plan → implement → tests is the NORMAL pipeline" from "5 spawns with no artifacts is a wedge."
- Codex acts: sends SIGTERM to its child mux-runner.
- The Bash environment codex is running in DOES have permission to send signals (no R-WSRC-class sandbox on codex's stdout-side actions).

This is structurally similar to Phase 1a-bis: codex parses something in its context window as a directive and executes. There, it was setup.js examples in skill prompts. Here, it's an inferred wedge diagnosis from session-dir observation.

## Why this is P1, not P3

- **Pipeline-killing**: a healthy pipeline mid-implement gets terminated for no real reason. We lost a worker mid-conformance-write (probably forfeited 5-10 min of in-progress codex thinking). Without the workaround, every codex-managed pipeline would die in the same shape.
- **No observable signal in mux-runner's own logs**: the only forensic evidence is what's left in the tmux pane scrollback after codex emitted the assistant text. Once the pane is closed or scrolled past, the diagnosis is lost. `state.json.activity` records `signal_received` (R-SOA) but NOT the sender or the rationale. A session that goes through this terminates with `exit_reason='signal:SIGTERM'` and looks like a normal external kill — operators can't tell it apart from `tmux kill-session` or `Ctrl-C`.
- **Workaround is fragile**: "run mux-runner directly under tmux pane, not under codex manager" works for me, but `/pickle-tmux` skill prompt teaches exactly that pattern, and `setup.js` invocations from `/pickle` (interactive) DO spawn under a codex/claude parent. Anyone using `/pickle` on codex backend can hit this.

## Solution (3 tickets)

### R-CCPM-1b-1 — Codex manager prompt: forbid signal-sending to its mux-runner child

Edit the codex manager's system prompt block (`extension/src/services/refinement-prompts.ts`, `composeManagerPromptFromSkill` helper, and the codex-specific Role Framing established by R-CCPM-1 `690e5c5c`) to include an explicit "DO NOT send signals to the mux-runner subprocess" directive with examples of the failure mode.

Add to the Role Framing block (codex backend only):
```
You orchestrate mux-runner.js as a tool, NOT as a peer process. Specifically:
- DO NOT send SIGTERM/SIGINT/SIGKILL to the mux-runner subprocess.
- DO NOT decide that mux-runner is wedged based on session-directory observation.
- DO NOT attempt to bypass mux-runner by spawning spawn-morty.js directly.
- Worker proliferation (multiple worker_session_*.log files per ticket) is NORMAL — it reflects the research → plan → implement → verify lifecycle, not a wedge.
- The only signal mux-runner needs is its own loop logic + operator-side `/eat-pickle` or `tmux kill-session`. You are not the watchdog.
- Real wedge detection lives in `circuit_breaker.json` (CLOSED → HALF_OPEN → OPEN) and `state.exit_reason`. If those don't say wedge, mux-runner isn't wedged.
```

Update:
- `extension/src/services/refinement-prompts.ts` — extend the codex-backend Role Framing block.
- `extension/src/bin/mux-runner.ts` — ensure the manager prompt composition path goes through `composeManagerPromptFromSkill` for codex (not raw skill prompt).
- New test: `extension/tests/services/codex-role-framing-no-signal.test.js` asserts the directive text is present in rendered codex manager prompts.

**Acceptance**
- `composeManagerPromptFromSkill('codex', ...)` output contains the literal string "DO NOT send SIGTERM/SIGINT/SIGKILL to the mux-runner subprocess".
- Test passes deterministically.

### R-CCPM-1b-2 — Signal-sender attribution in `signal_received` activity event

Extend the R-SOA `signal_received` payload (already shipped in v1.74.x) with sender attribution where available on macOS/Linux. When `signal_received` fires in mux-runner.ts's signal handler:

1. On Linux: read `/proc/self/status` for `TracerPid`, then `/proc/<TracerPid>/comm` to get sender process name.
2. On macOS: use `ps -o ppid,command= -p $$` plus `ps -A -o pid,ppid,command=` to find any process whose ppid chain contains us and whose command-line matches "codex" or known shell wrappers.
3. Best-effort; if unknown, leave `signal_sender_pid: null` and `signal_sender_cmd: null`.

The payload extension is non-breaking (additive optional fields).

Update:
- `extension/src/bin/mux-runner.ts` — `installShutdownHandlers().handleShutdown` adds sender lookup.
- `extension/src/types/activity-events.schema.json` — extend `signal_received.gate_payload` with optional `signal_sender_pid` and `signal_sender_cmd`.
- `extension/src/types/index.ts` — mirror.
- New test: `extension/tests/pipeline-runner-signal-attribution.test.js` (existing R-SOA test) gains a case asserting the codex-parent detection path returns a non-null sender_cmd containing "codex" when the test runner is itself the signaller and matches the pattern.

**Acceptance**
- `signal_received` events emitted by mux-runner SIGTERM include `gate_payload.signal_sender_pid` and `gate_payload.signal_sender_cmd` whenever the OS exposes that data.
- Operators can grep activity log for `signal_sender_cmd contains "codex"` to identify R-CCPM-1b incidents.
- R-SOA-5 dual-write invariant preserved (still logs to both `state.json.activity` and `pipeline-runner.log`).

### R-CCPM-1b-3 — `/pickle` and `/pickle-tmux` skill prompts: explicit warning + tmux-direct example for codex backend

Edit the skill prompts to:

- `.claude/commands/pickle.md` (interactive): when `--backend codex`, document the R-CCPM-1b workaround and recommend `/pickle-tmux` for any session longer than ~30 min on codex.
- `.claude/commands/pickle-tmux.md`: codex backend section reinforces "mux-runner runs in tmux pane 0 directly under zsh; codex is the worker process spawned by mux-runner, NOT the parent of mux-runner."

Update:
- `.claude/commands/pickle.md` — add Codex Backend section under Step 2.
- `.claude/commands/pickle-tmux.md` — add Codex Backend cautionary note under Step 4.
- `README.md` — update "Backends" section with R-CCPM-1b workaround pointer.

**Acceptance**
- Reading the skill prompts as an operator gives clear guidance: codex-backend → tmux-direct, not codex-managed pipeline.
- No regression in existing skill prompt behavior; flags + task wiring unchanged.

## Closer

Per R-CTSF-1 compliance: this PRD has no separate closer ticket. Manager-owned residuals (version bump, install.sh, MASTER_PLAN edit, gh release) tagged `[manager]` and excluded from worker-AC evaluation. The 3 implementation tickets above are the entire worker scope.

## Out of scope

- A general "manager cannot send signals to its own subprocesses" sandbox. That would be the right structural fix but requires a much larger surface change (codex process tree isolation, possibly via `setsid` or PID namespace). R-CCPM-1b-1's prompt-level guardrail is the pragmatic Phase-1 fix; structural sandboxing is a future R-CCPM-2 if Phase 1 isn't enough.
- Fixing codex's broader "agentic over-reach" problem. Codex is what it is; we work around it with prompt framing + observability.
- Phase-1a-bis residuals (R-CCPM-WH P3 follow-up). Those are independent.

## Risk / counter-arguments

- **What if codex IS right that the pipeline is wedged sometimes?** Possible but unobserved. The R-MRWG family (v1.75.1) gave mux-runner real wedge detection: bounded `runBetweenTicketFastTests`, stall detector, descendant-tree kill, orphan reaper, child-stall heartbeat. If those don't trip, mux-runner is not wedged. Codex's "wedge" intuition is at best redundant with those mechanisms; at worst (today) it's a false-positive that kills healthy work.
- **What if the prompt directive isn't enough?** R-CCPM-1b-2 (sender attribution) gives forensic proof when this recurs. If it recurs after R-CCPM-1b-1 ships, escalate to structural sandbox via a follow-up PRD. This is the same prompt-then-runtime-guard ladder R-CCPM Phase 1a used (R-CCPM-1 Role Framing + R-CCPM-2 LOG-only runtime observation).

## Trap doors

Each ticket's `conformance_*.md` MUST include explicit evidence for:
- R-CCPM-1b-1: rendered codex manager prompt contains the no-signal directive (grep -c output ≥ 1).
- R-CCPM-1b-2: synthetic SIGTERM in test fixture produces activity event with non-null `signal_sender_cmd`.
- R-CCPM-1b-3: skill prompts read with `head -100` show the Codex Backend section.
