From 69560e0bec34d967e2019c764a8f3e5f8b83efa4 Mon Sep 17 00:00:00 2001 From: Moe Charm Date: Thu, 4 Sep 2025 15:25:00 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Codex=E9=9D=9E=E5=90=8C=E6=9C=9F?= =?UTF-8?q?=E5=AE=9F=E8=A1=8C=E3=82=B7=E3=82=B9=E3=83=86=E3=83=A0=E3=81=AE?= =?UTF-8?q?=E6=94=B9=E8=89=AF=E7=89=88=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- AGENT.md | 26 ++++ tools/codex-async-notify.sh | 236 ++++++++++++++++++++++++++++++------ tools/codex-keep-two.sh | 55 +++++++++ 3 files changed, 282 insertions(+), 35 deletions(-) create mode 100644 tools/codex-keep-two.sh diff --git a/AGENT.md b/AGENT.md index 95a44cfd..fbeb7db9 100644 --- a/AGENT.md +++ b/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)を自動送信するので、確実に投稿される。 diff --git a/tools/codex-async-notify.sh b/tools/codex-async-notify.sh index 90b83ede..b160916d 100644 --- a/tools/codex-async-notify.sh +++ b/tools/codex-async-notify.sh @@ -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 " - echo "Example: $0 'Refactor MIR builder to 13 instructions'" + echo "Usage: $0 [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) - - # 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 + # 末尾表示行数(環境変数で上書き可)/ ミニマル通知モード + TAIL_N=${CODEX_NOTIFY_TAIL:-20} + MINIMAL=${CODEX_NOTIFY_MINIMAL:-1} + + # 通知内容を一時ファイルに組み立て(空行も保持) + 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!" @@ -94,4 +260,4 @@ echo "" echo "🔍 Check status:" echo " ps -p $ASYNC_PID" echo "" -echo "Codex is now working in the background..." \ No newline at end of file +echo "Codex is now working in the background..." diff --git a/tools/codex-keep-two.sh b/tools/codex-keep-two.sh new file mode 100644 index 00000000..6151e7b3 --- /dev/null +++ b/tools/codex-keep-two.sh @@ -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 "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 \"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/dev/null 2>&1 || true +done + +echo "[keep-two] now running: $(count_running)"