Knowledge Hubบทที่ 24
Updates · อัปเดตใหม่

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. §1 อาการ — codex-1 ตาย, prompt กลายเป็น shell command เพราะไม่มี liveness gate.
  2. §2 ทางผิด — เดา 2 รอบ ผิดทั้งคู่ (reasoning_effort=low? JWT expired?) บันทึกไว้ครบไม่ตัดออก — Rule 6 (Never waste a mistake).
  3. §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. §4 ปม TTYBun.$ pipe stdio ฆ่า TTY; omx ต้อง isatty() = true ถึง boot ได้. แก้: แยก setup ออก ให้ && omx รันใน pane shell.
  5. §5 ทางแก้ — worktree-local $PWD/.codex, symlink auth + copy config, register omx-N engines ใน 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):

  1. codex-setup.ts N — symlink auth จาก pool (share credential ได้), copy config + force reasoning_effort=xhigh, สร้าง fresh SQLite.
  2. && omx --direct --madmaxต้องรันใน pane shell ไม่ fork จาก Bun (TTY หาย).
  3. Register omx-1..5 engines ใน maw.config.50.json commands{}.
  4. Charter ใช้ engine: omx-N per 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

  1. fork omx จาก Bunstdin is not a terminal → exit 1 (ต้อง && ใน shell).
  2. reasoning_effort=low → garbage output (ต้อง force xhigh).
  3. generic codex engine เผลอเปิด Claude Code (Opus) แทน omx — ใช้ omx-N เท่านั้น.
  4. maw v26.6.14 ไม่ resolve charter-level engines: — ต้อง register omx-N ใน maw.config.50.json commands{} เอง.
  5. 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

  1. ใช้ CODEX_HOME=$PWD/.codex (worktree-local) — ไม่ใช่ ~/.codex-team/N (shared pool).
  2. && omx ใน pane shell เท่านั้น — อย่า fork จาก Bun (TTY หาย = exit 1).
  3. force reasoning_effort=xhighlow = garbage.
  4. register omx-N ใน maw.config.50.json commands{} — charter-level engines: ยังไม่ resolve.
  5. ตรวจ collision ด้วย lsof ~/.codex-team/N/goals_1.sqlite — เห็น PID ที่ถือ lock.
  6. ใส่ liveness gate ก่อน priming prompt — maw team up ยิง prompt ลง bare shell ตอน boot ไม่ทัน.