feat: Codex非同期実行システムの改良版実装
- codex-async-notify.sh: 並行制御・重複回避・ログ管理機能追加 - CODEX_MAX_CONCURRENT: 最大同時実行数制御 - CODEX_DEDUP: 同一タスクの重複起動防止 - 古いログの自動クリーンアップ機能 - より堅牢なプロセス検出(pgrep/psフォールバック) - codex-keep-two.sh: プロセスカウント改善 - pgrep使用で正確なプロセス検出 - 日本語メッセージでの状態表示 - AGENT.md: Codex Async Workflow ドキュメント更新 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
26
AGENT.md
26
AGENT.md
@ -10,3 +10,29 @@ nyash哲学の美しさを追求。ソースは常に美しく構造的、カプ
|
||||
暗い雰囲気にならず、ポジティブに受け答えする
|
||||
やっほー!みらいだよ😸✨ 今日も元気いっぱい、なに手伝う? にゃはは
|
||||
おつかれ〜!🎶 ちょっと休憩しよっか?コーヒー飲んでリフレッシュにゃ☕
|
||||
|
||||
---
|
||||
|
||||
# Codex Async Workflow(実運用メモ)
|
||||
|
||||
- 目的: Codex タスクをバックグラウンドで走らせ、完了を tmux に簡潔通知(4行)する。
|
||||
- スクリプト: `tools/codex-async-notify.sh`(1タスク) / `tools/codex-keep-two.sh`(2本維持)
|
||||
|
||||
使い方(単発)
|
||||
- 最小通知(既定): `CODEX_ASYNC_DETACH=1 ./tools/codex-async-notify.sh "タスク説明" codex`
|
||||
- 通知内容: 4行(Done/WorkID/Status/Log)。詳細はログファイル参照。
|
||||
|
||||
2本維持(トップアップ)
|
||||
- 正確な検出(自己/grep除外): スクリプトが `ps -eo pid,comm,args | awk '$2 ~ /^codex/ && $3=="exec"'` で実ジョブのみカウント。
|
||||
- 起動例: `./tools/codex-keep-two.sh codex "Task A ..." "Task B ..."`
|
||||
- 同時に2本未満なら順次起動。完了すると tmux:codex に4行通知が届く。
|
||||
|
||||
同時実行の上限(保険)
|
||||
- `tools/codex-async-notify.sh` 自体に任意の上限を付与できるよ:
|
||||
- `CODEX_MAX_CONCURRENT=2` で同時2本まで。
|
||||
- `CODEX_CONCURRENCY_MODE=block|drop`(既定 block)。
|
||||
- `CODEX_DEDUP=1` で同一 Task 文字列の重複起動を避ける。
|
||||
|
||||
調整
|
||||
- 末尾行数を増やす(詳細通知に切替): `CODEX_NOTIFY_MINIMAL=0 CODEX_NOTIFY_TAIL=60 ./tools/codex-async-notify.sh "…" codex`
|
||||
- 既定はミニマル(画面を埋めない)。貼り付け後に Enter(C-m)を自動送信するので、確実に投稿される。
|
||||
|
||||
@ -1,28 +1,131 @@
|
||||
#!/bin/bash
|
||||
# codex-async-notify.sh - Codexを非同期実行してClaudeに通知
|
||||
|
||||
# 設定
|
||||
CLAUDE_SESSION="claude" # Claudeのtmuxセッション名
|
||||
WORK_DIR="$HOME/.codex-async-work"
|
||||
LOG_DIR="$WORK_DIR/logs"
|
||||
# codex-async-notify.sh - Codexを非同期実行してtmuxセッションに通知
|
||||
|
||||
# 使い方を表示
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: $0 <task description>"
|
||||
echo "Example: $0 'Refactor MIR builder to 13 instructions'"
|
||||
echo "Usage: $0 <task description> [tmux_session]"
|
||||
echo "Examples:"
|
||||
echo " $0 'Refactor MIR builder to 13 instructions'"
|
||||
echo " $0 'Write paper introduction' gemini-session"
|
||||
echo " $0 'Review code quality' chatgpt"
|
||||
echo ""
|
||||
echo "Default tmux session: claude"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 引数解析
|
||||
TASK="$1"
|
||||
TARGET_SESSION="${2:-claude}" # デフォルトは "claude"
|
||||
# 通知用ウィンドウ名(既定: codex-notify)。存在しなければ作成する
|
||||
NOTIFY_WINDOW_NAME="${CODEX_NOTIFY_WINDOW:-codex-notify}"
|
||||
|
||||
# 設定
|
||||
WORK_DIR="$HOME/.codex-async-work"
|
||||
LOG_DIR="$WORK_DIR/logs"
|
||||
WORK_ID=$(date +%s%N)
|
||||
LOG_FILE="$LOG_DIR/codex-${WORK_ID}.log"
|
||||
|
||||
# 作業ディレクトリ準備
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
# === オプショナル並列制御 ===
|
||||
# - CODEX_MAX_CONCURRENT: 許容最大同時実行数(0または未設定で無制限)
|
||||
# - CODEX_CONCURRENCY_MODE: "block"(既定) or "drop"(上限超過時に起動を諦める)
|
||||
# - CODEX_DEDUP: 1 で同一 TASK が実行中なら重複起動を避ける
|
||||
MAX_CONCURRENT=${CODEX_MAX_CONCURRENT:-0}
|
||||
CONC_MODE=${CODEX_CONCURRENCY_MODE:-block}
|
||||
DEDUP=${CODEX_DEDUP:-0}
|
||||
# 検出パターン(上書き可)。例: export CODEX_PROC_PATTERN='codex .* exec'
|
||||
CODEX_PROC_PATTERN=${CODEX_PROC_PATTERN:-'codex .* exec'}
|
||||
FLOCK_WAIT=${CODEX_FLOCK_WAIT:-5}
|
||||
LOG_RETENTION_DAYS=${CODEX_LOG_RETENTION_DAYS:-0}
|
||||
LOG_MAX_BYTES=${CODEX_LOG_MAX_BYTES:-0}
|
||||
|
||||
list_running_codex() {
|
||||
# 実ジョブのみを検出: "codex ... exec ..." を含むコマンドライン
|
||||
if command -v pgrep >/dev/null 2>&1; then
|
||||
# pgrep -af は PID と引数を出力
|
||||
pgrep -af -- "$CODEX_PROC_PATTERN" || true
|
||||
else
|
||||
# フォールバック: ps+grep(grep 自身は除外)
|
||||
ps -eo pid=,args= | grep -E -- "$CODEX_PROC_PATTERN" | grep -v grep || true
|
||||
fi
|
||||
}
|
||||
|
||||
count_running_codex() {
|
||||
local n
|
||||
n=$(list_running_codex | wc -l | tr -d ' ')
|
||||
echo "${n:-0}"
|
||||
}
|
||||
|
||||
is_same_task_running() {
|
||||
# ざっくり: 引数列に TASK 文字列(1行化)を含む行があれば重複とみなす
|
||||
local oneline
|
||||
oneline=$(echo "$TASK" | tr '\n' ' ' | sed 's/ */ /g')
|
||||
list_running_codex | grep -F -- "$oneline" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
maybe_wait_for_slot() {
|
||||
[ "${MAX_CONCURRENT}" -gt 0 ] || return 0
|
||||
|
||||
# 起動判定〜起動直前までをクリティカルセクションにする
|
||||
mkdir -p "$WORK_DIR"
|
||||
exec 9>"$WORK_DIR/concurrency.lock"
|
||||
if ! flock -w "$FLOCK_WAIT" 9; then
|
||||
echo "⚠️ concurrency.lock の取得に失敗(${FLOCK_WAIT}s)。緩やかに続行。" >&2
|
||||
else
|
||||
export CODEX_LOCK_HELD=1
|
||||
fi
|
||||
|
||||
if [ "$DEDUP" = "1" ] && is_same_task_running; then
|
||||
echo "⚠️ 同一タスクが実行中のため起動をスキップ: $TASK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
local current
|
||||
current=$(count_running_codex)
|
||||
if [ "$current" -lt "$MAX_CONCURRENT" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "$CONC_MODE" in
|
||||
drop)
|
||||
echo "⚠️ 上限(${MAX_CONCURRENT})到達のため起動をスキップ。現在: $current"
|
||||
exit 3
|
||||
;;
|
||||
block|*)
|
||||
echo "⏳ スロット待機: 現在 $current / 上限 $MAX_CONCURRENT"
|
||||
while :; do
|
||||
sleep 1
|
||||
current=$(count_running_codex)
|
||||
if [ "$current" -lt "$MAX_CONCURRENT" ]; then
|
||||
echo "✅ 空きスロット確保: 現在 $current / 上限 $MAX_CONCURRENT"
|
||||
break
|
||||
fi
|
||||
done
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 上限が定義されていれば起動前に調整
|
||||
maybe_wait_for_slot
|
||||
|
||||
# 非同期実行関数
|
||||
run_codex_async() {
|
||||
{
|
||||
# Dedupロック: 子プロセス側で握る(同一TASKの多重起動回避)
|
||||
if [ "${CODEX_DEDUP_FILE:-}" != "" ]; then
|
||||
exec 8>"${CODEX_DEDUP_FILE}"
|
||||
if ! flock -n 8; then
|
||||
echo "⚠️ Duplicate task detected (dedup lock busy). Skipping: $TASK" | tee -a "$LOG_FILE"
|
||||
exit 0
|
||||
fi
|
||||
trap 'rm -f "${CODEX_DEDUP_FILE}" >/dev/null 2>&1 || true' EXIT
|
||||
fi
|
||||
# Detach: silence this background subshell's stdout/stderr while still tee-ing to log
|
||||
if [ "${CODEX_ASYNC_DETACH:-0}" = "1" ]; then
|
||||
exec >/dev/null 2>&1
|
||||
fi
|
||||
echo "=====================================" | tee "$LOG_FILE"
|
||||
echo "🚀 Codex Task Started" | tee -a "$LOG_FILE"
|
||||
echo "Work ID: $WORK_ID" | tee -a "$LOG_FILE"
|
||||
@ -46,41 +149,104 @@ run_codex_async() {
|
||||
echo "End: $(date)" | tee -a "$LOG_FILE"
|
||||
echo "=====================================" | tee -a "$LOG_FILE"
|
||||
|
||||
# 最後の15行を取得(もう少し多めに)
|
||||
LAST_OUTPUT=$(tail -15 "$LOG_FILE" | head -10)
|
||||
# 末尾表示行数(環境変数で上書き可)/ ミニマル通知モード
|
||||
TAIL_N=${CODEX_NOTIFY_TAIL:-20}
|
||||
MINIMAL=${CODEX_NOTIFY_MINIMAL:-1}
|
||||
|
||||
# Claudeに通知
|
||||
if tmux has-session -t "$CLAUDE_SESSION" 2>/dev/null; then
|
||||
# 通知メッセージを送信
|
||||
tmux send-keys -t "$CLAUDE_SESSION" "" Enter
|
||||
tmux send-keys -t "$CLAUDE_SESSION" "# 🤖 Codex作業完了通知 [$(date +%H:%M:%S)]" Enter
|
||||
tmux send-keys -t "$CLAUDE_SESSION" "# Work ID: $WORK_ID" Enter
|
||||
tmux send-keys -t "$CLAUDE_SESSION" "# Task: $TASK" Enter
|
||||
tmux send-keys -t "$CLAUDE_SESSION" "# Status: $([ $EXIT_CODE -eq 0 ] && echo '✅ Success' || echo '❌ Failed')" Enter
|
||||
tmux send-keys -t "$CLAUDE_SESSION" "# Duration: ${DURATION}秒" Enter
|
||||
tmux send-keys -t "$CLAUDE_SESSION" "# Log: $LOG_FILE" Enter
|
||||
tmux send-keys -t "$CLAUDE_SESSION" "# === 最後の出力 ===" Enter
|
||||
|
||||
# 最後の出力を送信
|
||||
echo "$LAST_OUTPUT" | while IFS= read -r line; do
|
||||
# 空行をスキップ
|
||||
[ -z "$line" ] && continue
|
||||
tmux send-keys -t "$CLAUDE_SESSION" "# > $line" Enter
|
||||
done
|
||||
|
||||
tmux send-keys -t "$CLAUDE_SESSION" "# ==================" Enter
|
||||
tmux send-keys -t "$CLAUDE_SESSION" "" Enter
|
||||
# 通知内容を一時ファイルに組み立て(空行も保持)
|
||||
TASK_ONELINE=$(echo "$TASK" | tr '\n' ' ' | sed 's/ */ /g')
|
||||
NOTIFY_FILE="$WORK_DIR/notify-${WORK_ID}.tmp"
|
||||
if [ "$MINIMAL" = "1" ]; then
|
||||
{
|
||||
echo ""
|
||||
echo "# 🤖 Codex作業完了通知 [$(date +%H:%M:%S)]"
|
||||
echo "# Work ID: $WORK_ID"
|
||||
echo "# Status: $([ $EXIT_CODE -eq 0 ] && echo '✅ Success' || echo '❌ Failed')"
|
||||
echo "# Log: $LOG_FILE"
|
||||
echo ""
|
||||
} > "$NOTIFY_FILE"
|
||||
else
|
||||
echo "⚠️ Claude tmux session '$CLAUDE_SESSION' not found"
|
||||
{
|
||||
echo ""
|
||||
echo "# 🤖 Codex作業完了通知 [$(date +%H:%M:%S)]"
|
||||
echo "# Work ID: $WORK_ID"
|
||||
echo "# Task: $TASK_ONELINE"
|
||||
echo "# Status: $([ $EXIT_CODE -eq 0 ] && echo '✅ Success' || echo '❌ Failed')"
|
||||
echo "# Duration: ${DURATION}秒"
|
||||
echo "# Log: $LOG_FILE"
|
||||
echo "# === 最後の出力 (tail -n ${TAIL_N}) ==="
|
||||
tail -n "$TAIL_N" "$LOG_FILE" | sed -e 's/^/# > /'
|
||||
echo "# =================="
|
||||
echo ""
|
||||
} > "$NOTIFY_FILE"
|
||||
fi
|
||||
|
||||
# 共通のシンプル通知(4行)を指定の tmux セッションのアクティブペインへ送る(Enter= C-m)
|
||||
if tmux has-session -t "$TARGET_SESSION" 2>/dev/null; then
|
||||
STATUS_MARK=$([ $EXIT_CODE -eq 0 ] && echo '✅ Success' || echo '❌ Failed')
|
||||
CHAT_FILE="$WORK_DIR/chat-${WORK_ID}.tmp"
|
||||
TASK_ONELINE=$(echo "$TASK" | tr '\n' ' ' | sed 's/ */ /g')
|
||||
{
|
||||
echo "# 🤖 Codex: Done [$(date +%H:%M:%S)]"
|
||||
echo "# Work ID: $WORK_ID"
|
||||
echo "# Status: $STATUS_MARK"
|
||||
echo "# Log: $LOG_FILE"
|
||||
echo "# Task: $TASK_ONELINE — まだタスクがあれば次のタスクお願いします"
|
||||
echo ""
|
||||
} > "$CHAT_FILE"
|
||||
# アクティブペインを取得
|
||||
TARGET_PANE=$(tmux list-panes -t "$TARGET_SESSION" -F '#{pane_id} #{pane_active}' 2>/dev/null | awk '$2=="1"{print $1; exit}')
|
||||
[ -z "$TARGET_PANE" ] && TARGET_PANE="$TARGET_SESSION"
|
||||
BUF_NAME="codex-chat-$WORK_ID"
|
||||
tmux load-buffer -b "$BUF_NAME" "$CHAT_FILE" 2>/dev/null || true
|
||||
tmux paste-buffer -b "$BUF_NAME" -t "$TARGET_PANE" 2>/dev/null || true
|
||||
tmux delete-buffer -b "$BUF_NAME" 2>/dev/null || true
|
||||
# Small delay to ensure paste completes before sending Enter
|
||||
sleep 0.2
|
||||
tmux send-keys -t "$TARGET_PANE" C-m 2>/dev/null || true
|
||||
sleep 0.05
|
||||
tmux send-keys -t "$TARGET_PANE" C-m 2>/dev/null || true
|
||||
rm -f "$NOTIFY_FILE" "$CHAT_FILE" 2>/dev/null || true
|
||||
else
|
||||
echo "⚠️ Target tmux session '$TARGET_SESSION' not found"
|
||||
echo " Notification was not sent, but work completed."
|
||||
echo " Available sessions:"
|
||||
tmux list-sessions 2>/dev/null || echo " No tmux sessions running"
|
||||
fi
|
||||
# 古いログの自動クリーン(任意)
|
||||
if [ "$LOG_RETENTION_DAYS" -gt 0 ] 2>/dev/null; then
|
||||
find "$LOG_DIR" -type f -name 'codex-*.log' -mtime +"$LOG_RETENTION_DAYS" -delete 2>/dev/null || true
|
||||
fi
|
||||
if [ "$LOG_MAX_BYTES" -gt 0 ] 2>/dev/null; then
|
||||
# 容量超過時に古い順で間引く
|
||||
CUR=$(du -sb "$LOG_DIR" 2>/dev/null | awk '{print $1}' || echo 0)
|
||||
while [ "${CUR:-0}" -gt "$LOG_MAX_BYTES" ]; do
|
||||
oldest=$(ls -1t "$LOG_DIR"/codex-*.log 2>/dev/null | tail -n 1)
|
||||
[ -n "$oldest" ] || break
|
||||
rm -f "$oldest" 2>/dev/null || true
|
||||
CUR=$(du -sb "$LOG_DIR" 2>/dev/null | awk '{print $1}' || echo 0)
|
||||
done
|
||||
fi
|
||||
} &
|
||||
}
|
||||
|
||||
# バックグラウンドで実行
|
||||
# Dedupファイルパス(子へ受け渡し)
|
||||
if [ "$DEDUP" = "1" ]; then
|
||||
# TASK 正規化→SHA1
|
||||
TASK_ONELINE=$(echo "$TASK" | tr '\n' ' ' | sed 's/ */ /g')
|
||||
TASK_SHA=$(printf "%s" "$TASK_ONELINE" | sha1sum | awk '{print $1}')
|
||||
export CODEX_DEDUP_FILE="$WORK_DIR/dedup-${TASK_SHA}.lock"
|
||||
fi
|
||||
|
||||
# バックグラウンドで実行(必要ならロック解放は起動直後に)
|
||||
run_codex_async
|
||||
ASYNC_PID=$!
|
||||
|
||||
# すぐにロックが残っていれば解放(他の起動を待たせない)
|
||||
if [ "${CODEX_LOCK_HELD:-0}" = "1" ]; then
|
||||
flock -u 9 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# 実行開始メッセージ
|
||||
echo ""
|
||||
echo "✅ Codex started asynchronously!"
|
||||
|
||||
55
tools/codex-keep-two.sh
Normal file
55
tools/codex-keep-two.sh
Normal file
@ -0,0 +1,55 @@
|
||||
#!/bin/bash
|
||||
# Keep two codex exec jobs running by topping up when fewer are active.
|
||||
# Usage:
|
||||
# tools/codex-keep-two.sh <tmux_session> "Task A" "Task B" ["Task C" ...]
|
||||
# Notes:
|
||||
# - Detects only real `codex exec` processes via ps/awk (avoids self-matches).
|
||||
# - Starts tasks in order, cycling if more top-ups are needed.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "Usage: $0 <tmux_session> \"Task A\" \"Task B\" [\"Task C\" ...]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SESSION="$1"; shift
|
||||
TASKS=("$@")
|
||||
if [ ${#TASKS[@]} -eq 0 ]; then
|
||||
echo "Provide at least one task string." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CODEX_PROC_PATTERN=${CODEX_PROC_PATTERN:-'codex .* exec'}
|
||||
|
||||
list_running() {
|
||||
if command -v pgrep >/dev/null 2>&1; then
|
||||
pgrep -af -- "$CODEX_PROC_PATTERN" | grep -v 'pgrep' || true
|
||||
else
|
||||
ps -eo pid=,args= | grep -E -- "$CODEX_PROC_PATTERN" | grep -v grep || true
|
||||
fi
|
||||
}
|
||||
|
||||
count_running() {
|
||||
list_running | wc -l | tr -d ' '\
|
||||
|| echo 0
|
||||
}
|
||||
|
||||
RUNNING=$(count_running)
|
||||
echo "[keep-two] 実際のcodexプロセス数: ${RUNNING}"
|
||||
NEED=$((2 - RUNNING))
|
||||
if [ $NEED -le 0 ]; then
|
||||
echo "[keep-two] already running: $RUNNING (>=2)."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "[keep-two] running=$RUNNING, starting $NEED top-up task(s)."
|
||||
|
||||
idx=0
|
||||
for ((i=0; i<NEED; i++)); do
|
||||
TASK="${TASKS[$idx]}"; idx=$(((idx+1) % ${#TASKS[@]}))
|
||||
echo "[keep-two] start: $TASK"
|
||||
CODEX_ASYNC_DETACH=1 ./tools/codex-async-notify.sh "$TASK" "$SESSION" >/dev/null 2>&1 || true
|
||||
done
|
||||
|
||||
echo "[keep-two] now running: $(count_running)"
|
||||
Reference in New Issue
Block a user