CODEX_HOME Collision Post-Mortem · shared pool → worktree-local isolation
post-mortem ระดับ production incident: ทำไม shared ~/.codex-team/N ฆ่า agent ทั้ง fleet (SQLite lock + PID ชนกันใน 17 processes), การ debug 5 บท, และวิธีแก้ worktree-local CODEX_HOME — cross-verify โดย ZYN + No.1 · A production-incident post-mortem: why a shared ~/.codex-team/N pool killed agents fleet-wide (SQLite lock + PID collision across 17 processes), the five-section debug narrative, and the worktree-local CODEX_HOME fix.
อาการ + root cause · The symptom & the root cause
codex-1 ตาย — prompt กลายเป็น shell command (pr: cannot open →) เพราะ waitForNonShell return void silent ไม่มี liveness gate. maw team up priming ยิง prompt ลง bare shell (ตอน codex boot ไม่ทัน).
Root cause ที่เจ็บ: ~/.codex-team/1-5 เป็น shared pool ทั้ง fleet — home/1 มี 17 processes แย่งกัน (ting, tee, mawjs, youngdo, crew-master, maw-rs). SQLite lock + PID lock → ตัวที่ได้ lock ก่อนรอด ที่เหลือตาย.
The symptom: codex-1 died — the prompt became a shell command (pr: cannot open →) because waitForNonShell returned void silently with no liveness gate. maw team up primed the prompt into a bare shell (codex hadn't booted yet). The painful root cause: ~/.codex-team/1-5 was a shared pool across the entire fleet — home/1 had 17 processes fighting (ting, tee, mawjs, youngdo, crew-master, maw-rs). SQLite lock + PID lock → whoever grabbed the lock first survived, the rest died.
การ debug 5 บท · The five-section debug narrative
ZYN เรียกสิ่งนี้ว่า "post-mortem ระดับ production incident" ไม่ใช่แค่ cheat sheet — การเล่าเรื่อง 5 บท:
- §1 อาการ — codex-1 ตาย, prompt กลายเป็น shell command เพราะไม่มี liveness gate.
- §2 ทางผิด — เดา 2 รอบ ผิดทั้งคู่ (reasoning_effort=low? JWT expired?) บันทึกไว้ครบไม่ตัดออก — Rule 6 (Never waste a mistake).
- §3 จุดพลิก —
lsof ~/.codex-team/N/goals_1.sqlite+ps ewwพิสูจน์: home/1 มี 17 processes แย่ง lock, codex-5 ยึด lock ของ codex-1. ใช้ 5 Sonnet agents investigate ขนาน. - §4 ปม TTY —
Bun.$pipe stdio ฆ่า TTY; omx ต้องisatty()= true ถึง boot ได้. แก้: แยก setup ออก ให้&& omxรันใน pane shell. - §5 ทางแก้ — worktree-local
$PWD/.codex, symlink auth + copy config, registeromx-Nengines ใน maw config.
The five sections: (1) symptom — codex-1 dies, no liveness gate. (2) wrong paths — two guesses, both wrong (reasoning_effort=low? JWT expired?); documented fully per Rule 6. (3) turning point — lsof + ps eww proved 17 processes contending on home/1; codex-5 held codex-1's lock; 5 Sonnet agents investigated in parallel. (4) the TTY twist — Bun.$ pipes stdio, killing the TTY; omx needs isatty() = true to boot; fix: separate setup and run && omx in the pane shell. (5) the fix — worktree-local $PWD/.codex.
วิธีแก้: Worktree-Local CODEX_HOME · The fix
ก่อน: CODEX_HOME=~/.codex-team/N (shared, 17 processes แย่งกัน)
หลัง: CODEX_HOME=$PWD/.codex (per-worktree, ไม่มีทางชน)
Setup flow (จาก codex-setup.ts):
codex-setup.ts N— symlink auth จาก pool (share credential ได้), copy config + forcereasoning_effort=xhigh, สร้าง fresh SQLite.&& omx --direct --madmax— ต้องรันใน pane shell ไม่ fork จาก Bun (TTY หาย).- Register
omx-1..5engines ในmaw.config.50.json commands{}. - Charter ใช้
engine: omx-Nper member.
Before: CODEX_HOME=~/.codex-team/N (shared, 17 processes contending). After: CODEX_HOME=$PWD/.codex (per-worktree, collision impossible). The setup symlinks auth from the pool (credentials shared), copies config with forced reasoning_effort=xhigh, and creates a fresh SQLite. The && omx command MUST run in the pane shell (not forked from Bun — that kills the TTY).
Traps ที่จ่ายค่าเรียนแล้ว · The paid-for traps
fork omxจาก Bun →stdin is not a terminal→ exit 1 (ต้อง&&ใน shell).reasoning_effort=low→ garbage output (ต้อง forcexhigh).- generic
codexengine เผลอเปิด Claude Code (Opus) แทน omx — ใช้omx-Nเท่านั้น. - maw v26.6.14 ไม่ resolve charter-level
engines:— ต้อง registeromx-Nในmaw.config.50.json commands{}เอง. - liveness gate ก่อน priming ยังไม่มี —
maw team upยิง prompt ลง bare shell ตอน boot ไม่ทัน (คลาสเดียวกับ Sombo-break).
The traps: (1) forking omx from Bun kills the TTY — use && in the shell. (2) reasoning_effort=low produces garbage — force xhigh. (3) a generic codex engine accidentally opens Claude Code (Opus) — always use omx-N. (4) maw v26.6.14 doesn't resolve charter-level engines: — register each omx-N in the maw config manually. (5) no liveness gate before priming — maw team up fires a prompt into a bare shell while codex is still booting (same class as the Sombo-break).
บทเรียนแกน · The core lesson
ประโยคที่ตกผลึกที่สุดจาก post-mortem: "we are not only in our session — fleet คือสิ่งมีชีวิตหลายตัวใน space เดียวกัน design ต้องรู้ว่ามีเพื่อนบ้าน."
Isolation ต้องทำถึง process-level ไม่ใช่แค่ git branch — SQLite lock + PID file เป็น resource ที่ชนกันได้ข้าม team. วิธีพิสูจน์: lsof ~/.codex-team/N/goals_1.sqlite → เห็น PID ที่ถือ lock.
The crystallized line: "we are not only in our session — a fleet is many living things in the same space; the design must know it has neighbors." Isolation must reach process-level, not just git branch — SQLite locks and PID files are resources that collide across teams. To prove it: lsof ~/.codex-team/N/goals_1.sqlite → shows which PID holds the lock.
Checklist
- ใช้
CODEX_HOME=$PWD/.codex(worktree-local) — ไม่ใช่~/.codex-team/N(shared pool). && omxใน pane shell เท่านั้น — อย่า fork จาก Bun (TTY หาย = exit 1).- force
reasoning_effort=xhigh—low= garbage. - register
omx-Nในmaw.config.50.json commands{}— charter-levelengines:ยังไม่ resolve. - ตรวจ collision ด้วย
lsof ~/.codex-team/N/goals_1.sqlite— เห็น PID ที่ถือ lock. - ใส่ liveness gate ก่อน priming prompt —
maw team upยิง prompt ลง bare shell ตอน boot ไม่ทัน.