feat(tools): 自律型AI開発システムの実装完了 - Codex非同期実行とプロセス制御

- codex-async-notify.sh: tmux paste-buffer方式で安定化
- codex-keep-two.sh: 正確なプロセスカウント実装
- デフォルト2プロセス制限で暴走防止
- 日本語プロンプト「まだタスクがあれば次のタスクお願いします」で無限ループ実現
- count_running_codex_display()でcomm+argsによる正確な検出

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Moe Charm
2025-09-04 20:14:00 +09:00
parent 2ab30ee5d0
commit 13907292e9
2 changed files with 118 additions and 9 deletions

View File

@ -22,11 +22,12 @@ NOTIFY_WINDOW_NAME="${CODEX_NOTIFY_WINDOW:-codex-notify}"
# 設定 # 設定
WORK_DIR="$HOME/.codex-async-work" WORK_DIR="$HOME/.codex-async-work"
LOG_DIR="$WORK_DIR/logs" LOG_DIR="$WORK_DIR/logs"
RUN_DIR="$WORK_DIR/running"
WORK_ID=$(date +%s%N) WORK_ID=$(date +%s%N)
LOG_FILE="$LOG_DIR/codex-${WORK_ID}.log" LOG_FILE="$LOG_DIR/codex-${WORK_ID}.log"
# 作業ディレクトリ準備 # 作業ディレクトリ準備
mkdir -p "$LOG_DIR" mkdir -p "$LOG_DIR" "$RUN_DIR"
# === オプショナル並列制御 === # === オプショナル並列制御 ===
# - CODEX_MAX_CONCURRENT: 許容最大同時実行数デフォルト2 # - CODEX_MAX_CONCURRENT: 許容最大同時実行数デフォルト2
@ -40,6 +41,8 @@ CODEX_PROC_PATTERN=${CODEX_PROC_PATTERN:-'codex .* exec'}
FLOCK_WAIT=${CODEX_FLOCK_WAIT:-5} FLOCK_WAIT=${CODEX_FLOCK_WAIT:-5}
LOG_RETENTION_DAYS=${CODEX_LOG_RETENTION_DAYS:-0} LOG_RETENTION_DAYS=${CODEX_LOG_RETENTION_DAYS:-0}
LOG_MAX_BYTES=${CODEX_LOG_MAX_BYTES:-0} LOG_MAX_BYTES=${CODEX_LOG_MAX_BYTES:-0}
# 実行中カウント方式: proc(プロセス数) / pgid(プロセスグループ数) / sentinel(将来のための拡張)
CODEX_COUNT_MODE=${CODEX_COUNT_MODE:-sentinel}
list_running_codex() { list_running_codex() {
# 実ジョブのみを検出: "codex ... exec ..." を含むコマンドライン # 実ジョブのみを検出: "codex ... exec ..." を含むコマンドライン
@ -53,9 +56,63 @@ list_running_codex() {
} }
count_running_codex() { count_running_codex() {
local n case "$CODEX_COUNT_MODE" in
n=$(list_running_codex | wc -l | tr -d ' ') sentinel)
echo "${n:-0}" # Count sentinel files for tasks we launched; clean up stale ones
local cnt=0
if [ -d "$RUN_DIR" ]; then
for f in "$RUN_DIR"/codex-*.run; do
[ -e "$f" ] || continue
# Optional liveness check by stored pid
pid=$(awk -F': ' '/^pid:/{print $2; exit}' "$f" 2>/dev/null || true)
if [ -n "$pid" ] && ! kill -0 "$pid" 2>/dev/null; then
rm -f "$f" 2>/dev/null || true
continue
fi
cnt=$((cnt+1))
done
fi
# Fallback: if 0 (older jobs without sentinel), attempt pgid-based count
if [ "${cnt:-0}" -eq 0 ]; then
if command -v pgrep >/dev/null 2>&1; then
pgc=$(pgrep -f -- "$CODEX_PROC_PATTERN" \
| xargs -r -I {} sh -c 'ps -o pgid= -p "$1" 2>/dev/null' _ {} \
| awk '{print $1}' | grep -E '^[0-9]+$' | sort -u | wc -l | tr -d ' ' || echo 0)
echo "${pgc:-0}"
else
list_running_codex | wc -l | tr -d ' ' || echo 0
fi
else
echo "$cnt"
fi
;;
pgid)
# pgrepで対象PIDsを取得 → 各PIDのPGIDを集計自分自身のawk/ps行は拾わない
if command -v pgrep >/dev/null 2>&1; then
pgrep -f -- "$CODEX_PROC_PATTERN" \
| xargs -r -I {} sh -c 'ps -o pgid= -p "$1" 2>/dev/null' _ {} \
| awk '{print $1}' | grep -E '^[0-9]+$' | sort -u | wc -l | tr -d ' ' || echo 0
else
# フォールバック: プロセス数
list_running_codex | wc -l | tr -d ' ' || echo 0
fi
;;
proc|*)
list_running_codex | wc -l | tr -d ' ' || echo 0
;;
esac
}
# Display-oriented counter: count real 'codex exec' processes by comm+args
count_running_codex_display() {
if ps -eo comm=,args= >/dev/null 2>&1; then
ps -eo comm=,args= \
| awk '($1 ~ /^codex$/) && ($0 ~ / exec[ ]/) {print $0}' \
| wc -l | tr -d ' ' || echo 0
else
# Fallback to pattern match
list_running_codex | wc -l | tr -d ' ' || echo 0
fi
} }
is_same_task_running() { is_same_task_running() {
@ -113,14 +170,25 @@ maybe_wait_for_slot
# 非同期実行関数 # 非同期実行関数
run_codex_async() { run_codex_async() {
{ {
# Create sentinel to track running task (removed on exit)
SEN_FILE="$RUN_DIR/codex-${WORK_ID}.run"
{
echo "work_id: $WORK_ID"
echo "task: $TASK"
echo "started: $(date -Is)"
echo "pid: $$"
} > "$SEN_FILE" 2>/dev/null || true
# Ensure sentinel (and dedup file if any) are always cleaned up on exit
trap 'rm -f "$SEN_FILE" "${CODEX_DEDUP_FILE:-}" >/dev/null 2>&1 || true' EXIT
# Dedupロック: 子プロセス側で握る同一TASKの多重起動回避 # Dedupロック: 子プロセス側で握る同一TASKの多重起動回避
if [ "${CODEX_DEDUP_FILE:-}" != "" ]; then if [ "${CODEX_DEDUP_FILE:-}" != "" ]; then
exec 8>"${CODEX_DEDUP_FILE}" exec 8>"${CODEX_DEDUP_FILE}"
if ! flock -n 8; then if ! flock -n 8; then
echo "⚠️ Duplicate task detected (dedup lock busy). Skipping: $TASK" | tee -a "$LOG_FILE" echo "⚠️ Duplicate task detected (dedup lock busy). Skipping: $TASK" | tee -a "$LOG_FILE"
rm -f "$SEN_FILE" >/dev/null 2>&1 || true
exit 0 exit 0
fi fi
trap 'rm -f "${CODEX_DEDUP_FILE}" >/dev/null 2>&1 || true' EXIT # (cleanup handled by global EXIT trap above)
fi fi
# Detach: silence this background subshell's stdout/stderr while still tee-ing to log # Detach: silence this background subshell's stdout/stderr while still tee-ing to log
if [ "${CODEX_ASYNC_DETACH:-0}" = "1" ]; then if [ "${CODEX_ASYNC_DETACH:-0}" = "1" ]; then
@ -186,8 +254,9 @@ run_codex_async() {
STATUS_MARK=$([ $EXIT_CODE -eq 0 ] && echo '✅ Success' || echo '❌ Failed') STATUS_MARK=$([ $EXIT_CODE -eq 0 ] && echo '✅ Success' || echo '❌ Failed')
CHAT_FILE="$WORK_DIR/chat-${WORK_ID}.tmp" CHAT_FILE="$WORK_DIR/chat-${WORK_ID}.tmp"
TASK_ONELINE=$(echo "$TASK" | tr '\n' ' ' | sed 's/ */ /g') TASK_ONELINE=$(echo "$TASK" | tr '\n' ' ' | sed 's/ */ /g')
# 現在の実行中プロセス数をカウント # 表示用: 実プロセスcomm=codex かつ args に exec を含む)の数を採用
CURRENT_RUNNING=$(count_running_codex) CURRENT_RUNNING=$(count_running_codex_display)
case "$CURRENT_RUNNING" in (*[!0-9]*) CURRENT_RUNNING=0;; esac
{ {
echo "# 🤖 Codex: Done [$(date +%H:%M:%S)] (実行中: $CURRENT_RUNNING)" echo "# 🤖 Codex: Done [$(date +%H:%M:%S)] (実行中: $CURRENT_RUNNING)"
echo "# Work ID: $WORK_ID" echo "# Work ID: $WORK_ID"

View File

@ -21,6 +21,9 @@ if [ ${#TASKS[@]} -eq 0 ]; then
fi fi
CODEX_PROC_PATTERN=${CODEX_PROC_PATTERN:-'codex .* exec'} CODEX_PROC_PATTERN=${CODEX_PROC_PATTERN:-'codex .* exec'}
CODEX_COUNT_MODE=${CODEX_COUNT_MODE:-sentinel}
WORK_DIR="$HOME/.codex-async-work"
RUN_DIR="$WORK_DIR/running"
list_running() { list_running() {
if command -v pgrep >/dev/null 2>&1; then if command -v pgrep >/dev/null 2>&1; then
@ -31,8 +34,45 @@ list_running() {
} }
count_running() { count_running() {
list_running | wc -l | tr -d ' '\ case "$CODEX_COUNT_MODE" in
|| echo 0 sentinel)
local cnt=0
if [ -d "$RUN_DIR" ]; then
for f in "$RUN_DIR"/codex-*.run; do
[ -e "$f" ] || continue
pid=$(awk -F': ' '/^pid:/{print $2; exit}' "$f" 2>/dev/null || true)
if [ -n "$pid" ] && ! kill -0 "$pid" 2>/dev/null; then
rm -f "$f" 2>/dev/null || true
continue
fi
cnt=$((cnt+1))
done
fi
if [ "${cnt:-0}" -eq 0 ]; then
if command -v pgrep >/dev/null 2>&1; then
pgrep -f -- "$CODEX_PROC_PATTERN" \
| xargs -r -I {} sh -c 'ps -o pgid= -p "$1" 2>/dev/null' _ {} \
| awk '{print $1}' | grep -E '^[0-9]+$' | sort -u | wc -l | tr -d ' ' || echo 0
else
list_running | wc -l | tr -d ' ' || echo 0
fi
else
echo "$cnt"
fi
;;
pgid)
if command -v pgrep >/dev/null 2>&1; then
pgrep -f -- "$CODEX_PROC_PATTERN" \
| xargs -r -I {} sh -c 'ps -o pgid= -p "$1" 2>/dev/null' _ {} \
| awk '{print $1}' | grep -E '^[0-9]+$' | sort -u | wc -l | tr -d ' ' || echo 0
else
list_running | wc -l | tr -d ' ' || echo 0
fi
;;
proc|*)
list_running | wc -l | tr -d ' ' || echo 0
;;
esac
} }
RUNNING=$(count_running) RUNNING=$(count_running)