Skip to content

fix: make automatic resume pane-safe - #4

Merged
VoidLight00 merged 2 commits into
mainfrom
fix/safe-pane-resume
Jul 21, 2026
Merged

fix: make automatic resume pane-safe#4
VoidLight00 merged 2 commits into
mainfrom
fix/safe-pane-resume

Conversation

@VoidLight00

@VoidLight00 VoidLight00 commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Summary

  • pin rate-limit recovery to the exact tmux pane and revalidate the Claude executable before sending input
  • make reset parsing and fallback fail closed, with atomic pane state and concurrency locks
  • add isolated tmux integration tests, a fail-closed master QA gate, and public install/update/rollback documentation
  • replace the mismatched JavaScript CodeQL workflow with the repository's real Bash/Python verification gate

Test plan

  • make test
  • make verify
  • isolated installed CLI runs nightguardian verify
  • live daemon restart and status check
  • GitHub repository doctor: P0=0

Local operator note

The duplicate claude-auto-retry shell wrapper was disabled locally while retaining the npm package for rollback. That machine-specific change is intentionally not part of this public repository.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • 새로운 기능

    • tmux의 각 pane을 독립적으로 감시하고, 정확한 Claude Code 세션만 안전하게 재개합니다.
    • 다양한 형식의 리셋 시간과 시간대를 인식합니다.
    • verify 검증 명령과 macOS 자동 시작 설정을 지원합니다.
    • 설치, 업데이트, 롤백, 삭제 및 안전 모델 안내를 확장했습니다.
  • 개선 사항

    • 확인되지 않은 프로세스나 pane에는 자동 입력을 보내지 않도록 안전성을 강화했습니다.
    • 중복 재개와 동시 실행을 방지하고, 시간 파싱 실패 시 기본적으로 대기합니다.
  • 품질 및 문서

    • 통합 테스트와 자동 품질·안전·문서 검증 게이트를 추가했습니다.
    • 사용법과 구성 예시를 전면 개편했습니다.

VoidLight00 and others added 2 commits June 20, 2026 16:30
증상: 5am 등 리밋 리셋 시각이 지나도, 화면에 확인 다이얼로그(Do you want to…? / ❯ 1.Yes)나
"How is Claude doing this session?" 피드백 프롬프트가 떠 있으면 가디언이 "⏸️ 확인 다이얼로그
대기 중"으로 영구 보류하고 resume를 보내지 않아 작업이 안 이어짐.

수정:
- 리셋 도래 시 영구 보류 금지. 오버레이 감지 시 Escape×2(취소=안전 기본값 + 입력창 비우기)
  후 무조건 resume 주입.
- GUARDIAN_FEEDBACK_RE 신설(피드백 오버레이 매칭), DIALOG_RE와 함께 판정.
- send-keys 텍스트→sleep1→C-m 분리 전송으로 Enter 레이스 방지.
- selftest에 피드백 회귀 케이스 4종 추가 → ALL GREEN.

검증: selftest exit0, 합성 세션 end-to-end(Escape후 resume 전송), 실제 Claude Code TUI에서
동일 키 시퀀스 제출·응답(PONG) 확인.

함께 커밋: 이전 세션들의 미커밋 nightguardian 작업(keepalive.sh, parse_reset.py,
CLI/Makefile, launchd plist) — 현재 동작 중인 데몬 상태를 정본화.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Why: session-wide input and duplicate retry ownership could send commands to the wrong terminal, so NightGuardian now pins and verifies one Claude pane with fail-closed QA before public use.

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

NightGuardian이 세션 단위 처리에서 pane 단위 감시·재개로 전환되었습니다. 리셋 시간 파서, Claude 프로세스 검증, pane별 상태·락·쿨다운, CLI 운영 명령, 통합 테스트와 fail-closed QA 게이트가 추가되었습니다.

Changes

NightGuardian 안전 재개

Layer / File(s) Summary
리셋 파서와 watcher 기반
src/parse_reset.py, src/guardian-watch.sh
상대·절대 시간과 타임존을 처리하는 파서 및 환경변수 기반 watcher 상태·프로세스 검증 기반이 추가되었습니다.
Pane 감지와 안전 재개
src/guardian-watch.sh, tests/integration_tmux.sh
모든 pane을 탐색하고, 검증된 Claude pane에만 resume을 전송하며 pane별 상태·락·쿨다운과 동시성 시나리오를 검증합니다.
CLI와 자동 시작 운영
src/nightguardian, src/keepalive.sh, config/*
verify, autostart, pane 상태 조회와 keepalive 재시작 동작 및 macOS LaunchAgent 설정이 추가되었습니다.
테스트와 마스터 검증 게이트
Makefile, gates/*, .github/workflows/verify.yml
셸·Python 검사, 안전성·시크릿·문서·통합 테스트 게이트와 전체 verify 실행 경로가 추가되었습니다.
요구사항과 공개 문서
REQUIREMENTS.md, README.md, CHANGELOG.md, FAILURE_LOG.md, .gitignore
요구사항, 설치·운영 문서, 릴리스 기록, 실패 기록과 Python 생성물 제외 규칙이 갱신되었습니다.

Estimated code review effort: 5 (Critical) | ~120 minutes

Suggested reviewers: son

Poem

토끼가 pane 사이를 깡충,
안전한 Claude만 찾아요.
시간 읽고, 잠금 걸고,
올바른 곳에 키를 톡톡.
게이트도 모두 통과했네! 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 자동 재개를 tmux 패널 단위로 안전하게 바꾸는 핵심 변경을 간결하게 요약합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/safe-pane-resume

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@VoidLight00
VoidLight00 merged commit b7431df into main Jul 21, 2026
1 of 2 checks passed
@VoidLight00
VoidLight00 deleted the fix/safe-pane-resume branch July 21, 2026 17:07

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (4)
.github/workflows/verify.yml (1)

16-16: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

보안 강화를 위해 actions/checkoutpersist-credentials: false를 설정하세요.

현재 설정에서는 GitHub Actions 토큰이 .git 디렉터리에 남아 이후 단계나 써드파티 도구에 의해 노출될 수 있습니다. 코드를 체크아웃할 때 인증 정보가 로컬에 지속되지 않도록 명시적으로 비활성화하는 것을 권장합니다.

🔒 제안하는 수정안
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v4
+        with:
+          persist-credentials: false
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/verify.yml at line 16, Update the actions/checkout step to
explicitly disable persisted credentials by configuring persist-credentials to
false, ensuring the GitHub Actions token is not retained in the local .git
directory.

Source: Linters/SAST tools

src/keepalive.sh (1)

24-28: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

nightguardian stop 명령을 사용하여 tmux 세션 이름을 캡슐화하고 로그 기록을 통합하세요.

현재 스크립트에 forge-night라는 tmux 세션 이름이 하드코딩되어 있습니다. 이로 인해 CLI 내부에 설정된 $TMUX_SESSION 환경변수와 세션 이름이 다를 경우 비정상 세션 정리가 제대로 이루어지지 않을 수 있습니다. 하드코딩된 tmux kill-session 대신 nightguardian stop을 호출하여 세션 이름을 캡슐화하는 것을 권장합니다.

또한, 여러 번 반복되는 >> "$LOG" 리다이렉션을 하나의 중괄호 { ... } >> "$LOG" 블록으로 묶으면 가독성이 향상되고 I/O 처리가 더 효율적이 됩니다.

🛠 제안하는 수정안
-ts="$(date '+%Y-%m-%d %H:%M:%S')"
-tmux kill-session -t forge-night 2>/dev/null || true
-echo "[$ts] guardian 프로세스 부재 감지 → 재시작" >> "$LOG"
-nightguardian start >> "$LOG" 2>&1
-echo "[$ts] 재시작 결과 pid: $(pgrep -f guardian-watch.sh 2>/dev/null || echo NONE)" >> "$LOG"
+{
+  ts="$(date '+%Y-%m-%d %H:%M:%S')"
+  nightguardian stop 2>/dev/null || true
+  echo "[$ts] guardian 프로세스 부재 감지 → 재시작"
+  nightguardian start 2>&1
+  echo "[$ts] 재시작 결과 pid: $(pgrep -f guardian-watch.sh 2>/dev/null || echo NONE)"
+} >> "$LOG"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/keepalive.sh` around lines 24 - 28, Replace the hardcoded tmux
kill-session call in the keepalive restart flow with nightguardian stop,
preserving the existing tolerant behavior when no session exists. Group the
related timestamp, restart, and result log commands in a single { ... } >>
"$LOG" block, while keeping nightguardian start output redirected appropriately
and retaining the existing restart PID message.

Source: Linters/SAST tools

src/guardian-watch.sh (1)

302-319: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

미사용 지역 변수 row.

Shellcheck(SC2034)가 지적하듯 302-303행에서 선언된 row는 이후 어디서도 사용되지 않습니다. 나머지 스킵 로직(session/window_index/pane_index/pane_id 파싱, GUARDIAN_SKIP_SESSIONS 글롭 매칭)은 정상입니다.

🧹 제안
 scan_all_panes() {
-  local row session window_index pane_index pane_id
+  local session window_index pane_index pane_id
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/guardian-watch.sh` around lines 302 - 319, Remove the unused local
variable row from scan_all_panes, leaving session, window_index, pane_index, and
pane_id unchanged so the existing pane parsing and skip logic remain intact.

Source: Linters/SAST tools

tests/integration_tmux.sh (1)

45-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

force_due에 실행되지 않는 데드 코드가 남아있음.

reset.join(line.split("=", 1)[:1]) + "=" + reset if False else (...) 구문에서 if False 앞의 표현식은 절대 평가되지 않습니다. 실질적으로는 else 분기만 항상 실행되므로, 디버깅 흔적으로 보이는 앞부분을 제거해 가독성을 높이는 것을 권장합니다.

🧹 정리 제안
 lines = path.read_text().splitlines()
-path.write_text("\n".join(reset.join(line.split("=", 1)[:1]) + "=" + reset if False else (f"reset_epoch={reset}" if line.startswith("reset_epoch=") else line) for line in lines) + "\n")
+path.write_text(
+    "\n".join(
+        f"reset_epoch={reset}" if line.startswith("reset_epoch=") else line
+        for line in lines
+    )
+    + "\n"
+)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration_tmux.sh` around lines 45 - 53, Remove the unreachable
expression before “if False” in the Python transformation inside force_due,
leaving only the logic that replaces lines starting with “reset_epoch=” while
preserving all other lines unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@FAILURE_LOG.md`:
- Around line 7-10: FAILURE_LOG.md의 NG-001부터 NG-004까지 기록된 날짜를 실제 발생일인
2026-07-21로 수정하세요. 아직 발생하지 않은 검증 시나리오라면 해당 항목을 실패 기록에서 제거하고 별도 계획 문서로 분리하세요.

In `@README.md`:
- Around line 46-68: Reorder the README installation steps so the PATH export
for ~/.local/bin appears before the nightguardian start command, or invoke the
installed CLI via its absolute path. Ensure users do not attempt to run
nightguardian until the CLI directory is available in PATH.

In `@src/guardian-watch.sh`:
- Around line 268-285: Throttle the “HOLD: reset time missing” log in the
rate-limit handling flow around parse_reset_time and GUARDIAN_FALLBACK_MODE,
using the existing 300-second throttling pattern from the nearby “Waiting” log.
Keep the hold behavior unchanged while preventing the message from being emitted
on every scan.
- Around line 152-160: Update acquire_pane_lock to detect stale lock directories
by checking the existing lock_dir mtime and remove locks older than the
configured threshold (for example, 120 seconds) before retrying mkdir. Preserve
the current atomic mkdir acquisition behavior and ensure only stale locks are
reclaimed, allowing resume_pane to proceed after crashes.
- Around line 192-211: After the two Escape operations in the overlay-closing
flow, recapture the target pane content and recheck it against
GUARDIAN_DIALOG_RE and GUARDIAN_FEEDBACK_RE before sending the resume prompt. If
either pattern remains, abort safely using the existing cleanup and no-prompt
path; retain pane_runs_claude as an additional process-state check.
- Around line 350-358: Update cleanup_legacy_state so pane_*.state files are
removed based on their stored reset_epoch rather than the file mtime. Parse each
state file’s reset_epoch and delete it only when that reset time has expired,
preserving valid pending states such as weekly restrictions across daemon
restarts; retain the existing cleanup for legacy session files.

---

Nitpick comments:
In @.github/workflows/verify.yml:
- Line 16: Update the actions/checkout step to explicitly disable persisted
credentials by configuring persist-credentials to false, ensuring the GitHub
Actions token is not retained in the local .git directory.

In `@src/guardian-watch.sh`:
- Around line 302-319: Remove the unused local variable row from scan_all_panes,
leaving session, window_index, pane_index, and pane_id unchanged so the existing
pane parsing and skip logic remain intact.

In `@src/keepalive.sh`:
- Around line 24-28: Replace the hardcoded tmux kill-session call in the
keepalive restart flow with nightguardian stop, preserving the existing tolerant
behavior when no session exists. Group the related timestamp, restart, and
result log commands in a single { ... } >> "$LOG" block, while keeping
nightguardian start output redirected appropriately and retaining the existing
restart PID message.

In `@tests/integration_tmux.sh`:
- Around line 45-53: Remove the unreachable expression before “if False” in the
Python transformation inside force_due, leaving only the logic that replaces
lines starting with “reset_epoch=” while preserving all other lines unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 42fb74d0-cd89-47b8-9655-938831cbf684

📥 Commits

Reviewing files that changed from the base of the PR and between 0c3b094 and f10f9fb.

📒 Files selected for processing (23)
  • .github/workflows/codeql.yml
  • .github/workflows/verify.yml
  • .gitignore
  • CHANGELOG.md
  • FAILURE_LOG.md
  • Makefile
  • README.md
  • REQUIREMENTS.md
  • config/com.voidlight.nightguardian.plist
  • config/sessions.json.template
  • gates/docs_gate.sh
  • gates/lib/gate-lib.sh
  • gates/no_vertical_stripe_gate.sh
  • gates/quality_gate.sh
  • gates/safety_gate.sh
  • gates/secrets_gate.sh
  • gates/tests_gate.sh
  • gates/verify_nightguardian.sh
  • src/guardian-watch.sh
  • src/keepalive.sh
  • src/nightguardian
  • src/parse_reset.py
  • tests/integration_tmux.sh
💤 Files with no reviewable changes (1)
  • .github/workflows/codeql.yml

Comment thread FAILURE_LOG.md
Comment on lines +7 to +10
| 2026-07-22 | NG-001 | Concurrent watchers sent duplicate resume input | No atomic claim around due pane state | Added pane-scoped `mkdir` lock and atomic state writes | `tests_gate.sh`, `safety_gate.sh` |
| 2026-07-22 | NG-002 | Installed `nightguardian verify` resolved the wrong root | CLI derived paths from the symlink location | Resolve the executable realpath before deriving project root | `tests_gate.sh` |
| 2026-07-22 | NG-003 | Claude-looking process arguments could pass validation | Full command lines were matched as unstructured text | Match executable paths/names from `ps comm` only | `tests_gate.sh`, `safety_gate.sh` |
| 2026-07-22 | NG-004 | An unrelated clock time could be parsed as reset time | Absolute-time parser searched the whole pane | Scope absolute times to reset/retry context | `tests_gate.sh` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

실패 기록의 날짜를 수정하세요.

현재 날짜는 2026년 7월 21일인데 모든 항목이 2026년 7월 22일로 기록되어 있습니다. 실제 발생일을 사용하거나, 아직 발생하지 않은 검증 시나리오라면 별도 계획 문서로 분리해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FAILURE_LOG.md` around lines 7 - 10, FAILURE_LOG.md의 NG-001부터 NG-004까지 기록된
날짜를 실제 발생일인 2026-07-21로 수정하세요. 아직 발생하지 않은 검증 시나리오라면 해당 항목을 실패 기록에서 제거하고 별도 계획
문서로 분리하세요.

Comment thread README.md
Comment on lines 46 to +68
```bash
# 1. 클론/다운로드 후
git clone https://github.com/VoidLight00/nightguardian.git
cd nightguardian
make test
make install
nightguardian start
```

`make install` creates:

```text
~/.forgechain-nightguardian/
├── bin -> <checkout>/src
├── config -> <checkout>/config
├── manifest/
└── logs/
```

# 또는 수동
mkdir -p ~/.forgechain-nightguardian
ln -s "$(pwd)/src" ~/.forgechain-nightguardian/bin
ln -s "$(pwd)/config" ~/.forgechain-nightguardian/config
mkdir -p ~/.forgechain-nightguardian/{manifest,logs}
Add the CLI directory to your shell if it is not already present:

# 2. PATH 추가
export PATH="${HOME}/.local/bin:${PATH}"
```bash
export PATH="$HOME/.local/bin:$PATH"
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

PATH 설정 전에 CLI를 실행하지 마세요.

새 셸에서는 ~/.local/bin이 PATH에 없을 수 있는데, 현재 문서는 nightguardian start를 먼저 실행한 뒤 PATH 설정을 안내합니다. PATH 블록을 nightguardian start보다 앞에 배치하거나 설치된 CLI의 절대 경로를 사용해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 46 - 68, Reorder the README installation steps so the
PATH export for ~/.local/bin appears before the nightguardian start command, or
invoke the installed CLI via its absolute path. Ensure users do not attempt to
run nightguardian until the CLI directory is available in PATH.

Comment thread src/guardian-watch.sh
Comment on lines +152 to +160
acquire_pane_lock() {
local pane_id="$1" lock_dir
lock_dir="${MANIFEST_DIR}/pane_$(safe_id "$pane_id").lock"
if mkdir "$lock_dir" 2>/dev/null; then
printf '%s\n' "$lock_dir"
return 0
fi
return 1
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

pane 락에 만료(staleness) 회수 로직이 없어, 크래시 시 해당 pane이 영구적으로 막힘.

mkdir 기반 락은 정상 종료 경로(173-233의 각 return 지점)에서만 rmdir로 해제됩니다. 데몬이 resume_pane 실행 중 SIGKILL 등으로 중단되면 락 디렉터리가 영구히 남고, 이후 모든 스캔에서 acquire_pane_lock이 항상 실패해 resume_pane은 176행에서 조용히 return 0합니다. 결과적으로 해당 pane은 로그 한 줄도 남기지 않고 영원히 자동 재개 대상에서 제외됩니다. "fail-closed"가 아니라 "silent永久 hold"가 되는 셈이라, 락 디렉터리의 mtime을 확인해 일정 시간(예: 120초) 이상 지난 락은 회수하는 처리를 권장합니다.

🔒 오래된 락 회수 제안
 acquire_pane_lock() {
-  local pane_id="$1" lock_dir
+  local pane_id="$1" lock_dir age lock_mtime
   lock_dir="${MANIFEST_DIR}/pane_$(safe_id "$pane_id").lock"
   if mkdir "$lock_dir" 2>/dev/null; then
     printf '%s\n' "$lock_dir"
     return 0
   fi
+  lock_mtime=$(stat -c %Y "$lock_dir" 2>/dev/null || stat -f %m "$lock_dir" 2>/dev/null || printf '%s' "$(date +%s)")
+  age=$(( $(date +%s) - lock_mtime ))
+  if [ "$age" -gt 120 ] && rmdir "$lock_dir" 2>/dev/null && mkdir "$lock_dir" 2>/dev/null; then
+    printf '%s\n' "$lock_dir"
+    return 0
+  fi
   return 1
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
acquire_pane_lock() {
local pane_id="$1" lock_dir
lock_dir="${MANIFEST_DIR}/pane_$(safe_id "$pane_id").lock"
if mkdir "$lock_dir" 2>/dev/null; then
printf '%s\n' "$lock_dir"
return 0
fi
return 1
}
acquire_pane_lock() {
local pane_id="$1" lock_dir age lock_mtime
lock_dir="${MANIFEST_DIR}/pane_$(safe_id "$pane_id").lock"
if mkdir "$lock_dir" 2>/dev/null; then
printf '%s\n' "$lock_dir"
return 0
fi
lock_mtime=$(stat -c %Y "$lock_dir" 2>/dev/null || stat -f %m "$lock_dir" 2>/dev/null || printf '%s' "$(date +%s)")
age=$(( $(date +%s) - lock_mtime ))
if [ "$age" -gt 120 ] && rmdir "$lock_dir" 2>/dev/null && mkdir "$lock_dir" 2>/dev/null; then
printf '%s\n' "$lock_dir"
return 0
fi
return 1
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/guardian-watch.sh` around lines 152 - 160, Update acquire_pane_lock to
detect stale lock directories by checking the existing lock_dir mtime and remove
locks older than the configured threshold (for example, 120 seconds) before
retrying mkdir. Preserve the current atomic mkdir acquisition behavior and
ensure only stale locks are reclaimed, allowing resume_pane to proceed after
crashes.

Comment thread src/guardian-watch.sh
Comment on lines +192 to 211
resume_prompt=$(get_resume_prompt "$session")
if printf '%s' "$pane_content" | grep -qiE "$GUARDIAN_DIALOG_RE|$GUARDIAN_FEEDBACK_RE"; then
log "[$session $pane_id] Closing Claude overlay before resume."
tmux_run send-keys -t "$pane_id" Escape 2>/dev/null || {
rmdir "$lock_dir" 2>/dev/null || true
return 1
}
sleep 1
tmux_run send-keys -t "$pane_id" Escape 2>/dev/null || {
rmdir "$lock_dir" 2>/dev/null || true
return 1
}
sleep 1
pane_runs_claude "$pane_id" || {
log "[$session $pane_id] BLOCKED: target changed after overlay close; no prompt sent."
rm -f "$state_file"
else
local remaining=$((reset_epoch + 60 - now_epoch))
local min=$((remaining / 60))
if [ "$((now_epoch % 300))" -lt "$CHECK_INTERVAL" ]; then
log "[$session] ⏳ Still waiting... ${min}m until reset"
fi
fi
rmdir "$lock_dir" 2>/dev/null || true
return 0
}
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Escape 전송 후 오버레이가 실제로 닫혔는지 재검증하지 않음.

두 번의 Escape 전송 후에는 pane_runs_claude(프로세스 생존)만 재확인합니다. GUARDIAN_DIALOG_RE/GUARDIAN_FEEDBACK_RE에 대해 pane 콘텐츠를 다시 캡처해 오버레이가 실제로 사라졌는지는 검증하지 않습니다. Escape가 무시되거나 다른 형태의 오버레이(예: 다중 단계 확인창)라면, 다이얼로그가 여전히 떠 있는 상태로 resume 프롬프트 텍스트가 그대로 입력될 위험이 있습니다. 이는 이 PR의 핵심 목표("pane-safe" 자동 재개)와 직접 충돌하는 지점이라 recapture 후 재검사를 권장합니다.

🛡 재검증 제안
     sleep 1
-    pane_runs_claude "$pane_id" || {
+    pane_content=$(capture_pane "$pane_id")
+    if printf '%s' "$pane_content" | grep -qiE "$GUARDIAN_DIALOG_RE|$GUARDIAN_FEEDBACK_RE"; then
+      log "[$session $pane_id] BLOCKED: overlay still visible after Escape; no prompt sent."
+      rmdir "$lock_dir" 2>/dev/null || true
+      return 1
+    fi
+    pane_runs_claude "$pane_id" || {
       log "[$session $pane_id] BLOCKED: target changed after overlay close; no prompt sent."
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
resume_prompt=$(get_resume_prompt "$session")
if printf '%s' "$pane_content" | grep -qiE "$GUARDIAN_DIALOG_RE|$GUARDIAN_FEEDBACK_RE"; then
log "[$session $pane_id] Closing Claude overlay before resume."
tmux_run send-keys -t "$pane_id" Escape 2>/dev/null || {
rmdir "$lock_dir" 2>/dev/null || true
return 1
}
sleep 1
tmux_run send-keys -t "$pane_id" Escape 2>/dev/null || {
rmdir "$lock_dir" 2>/dev/null || true
return 1
}
sleep 1
pane_runs_claude "$pane_id" || {
log "[$session $pane_id] BLOCKED: target changed after overlay close; no prompt sent."
rm -f "$state_file"
else
local remaining=$((reset_epoch + 60 - now_epoch))
local min=$((remaining / 60))
if [ "$((now_epoch % 300))" -lt "$CHECK_INTERVAL" ]; then
log "[$session] ⏳ Still waiting... ${min}m until reset"
fi
fi
rmdir "$lock_dir" 2>/dev/null || true
return 0
}
fi
resume_prompt=$(get_resume_prompt "$session")
if printf '%s' "$pane_content" | grep -qiE "$GUARDIAN_DIALOG_RE|$GUARDIAN_FEEDBACK_RE"; then
log "[$session $pane_id] Closing Claude overlay before resume."
tmux_run send-keys -t "$pane_id" Escape 2>/dev/null || {
rmdir "$lock_dir" 2>/dev/null || true
return 1
}
sleep 1
tmux_run send-keys -t "$pane_id" Escape 2>/dev/null || {
rmdir "$lock_dir" 2>/dev/null || true
return 1
}
sleep 1
pane_content=$(capture_pane "$pane_id")
if printf '%s' "$pane_content" | grep -qiE "$GUARDIAN_DIALOG_RE|$GUARDIAN_FEEDBACK_RE"; then
log "[$session $pane_id] BLOCKED: overlay still visible after Escape; no prompt sent."
rmdir "$lock_dir" 2>/dev/null || true
return 1
fi
pane_runs_claude "$pane_id" || {
log "[$session $pane_id] BLOCKED: target changed after overlay close; no prompt sent."
rm -f "$state_file"
rmdir "$lock_dir" 2>/dev/null || true
return 0
}
fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/guardian-watch.sh` around lines 192 - 211, After the two Escape
operations in the overlay-closing flow, recapture the target pane content and
recheck it against GUARDIAN_DIALOG_RE and GUARDIAN_FEEDBACK_RE before sending
the resume prompt. If either pattern remains, abort safely using the existing
cleanup and no-prompt path; retain pane_runs_claude as an additional
process-state check.

Comment thread src/guardian-watch.sh
Comment on lines 268 to +285

while true; do
local session_list
session_list=$(tmux ls 2>/dev/null | cut -d: -f1 || true)
if ! pane_runs_claude "$pane_id"; then
log "[$session $pane_id] IGNORED: limit text found outside a verified Claude pane."
return 0
fi

if [ -z "$session_list" ]; then
if [ "$(($(date +%s) % 60))" -lt "$CHECK_INTERVAL" ]; then
log "No active tmux sessions found."
fi
reset_epoch=$(parse_reset_time "$pane_content")
if [ -n "$reset_epoch" ] && [ "$reset_epoch" -gt 0 ] 2>/dev/null; then
atomic_write_state "$state_file" rate_limited "$reset_epoch" "$now_epoch" "$pane_id" "$session" "$window_index" "$pane_index"
log "[$session $pane_id] Rate limit detected; exact pane pinned until epoch $reset_epoch."
elif [ "$GUARDIAN_FALLBACK_MODE" = "resume" ]; then
reset_epoch=$((now_epoch + GUARDIAN_FALLBACK_WAIT))
atomic_write_state "$state_file" rate_limited_fallback "$reset_epoch" "$now_epoch" "$pane_id" "$session" "$window_index" "$pane_index"
log "[$session $pane_id] Reset time missing; opt-in fallback scheduled in ${GUARDIAN_FALLBACK_WAIT}s."
else
for session in $session_list; do
# Skip the guardian's own session and control sessions.
# 추가 제외 패턴은 GUARDIAN_SKIP_SESSIONS(공백 구분 glob)로 지정.
case "$session" in
forge-night|claude-retry-*|main) continue ;;
esac
for _skip in ${GUARDIAN_SKIP_SESSIONS:-}; do
case "$session" in $_skip) continue 2 ;; esac
done
log "[$session $pane_id] HOLD: reset time missing; automatic fallback is disabled."
fi
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

HOLD 분기 로그가 스캔 주기마다 무제한 반복 기록됨.

reset 시각 파싱 실패 + GUARDIAN_FALLBACK_MODE=hold(기본값)인 경우 state/cooldown 파일이 전혀 기록되지 않습니다. limit 텍스트가 pane에 남아있는 동안 매 스캔(기본 CHECK_INTERVAL=30초)마다 283행의 "HOLD..." 로그가 스로틀 없이 계속 기록됩니다. 바로 위 260행의 "Waiting" 로그는 동일한 300초 스로틀 패턴을 이미 사용하므로, 같은 방식을 적용해 로그 파일 무한 증가를 막는 것을 권장합니다.

♻ 스로틀 적용 제안
   else
-    log "[$session $pane_id] HOLD: reset time missing; automatic fallback is disabled."
+    if [ "$((now_epoch % 300))" -lt "$CHECK_INTERVAL" ]; then
+      log "[$session $pane_id] HOLD: reset time missing; automatic fallback is disabled."
+    fi
   fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
while true; do
local session_list
session_list=$(tmux ls 2>/dev/null | cut -d: -f1 || true)
if ! pane_runs_claude "$pane_id"; then
log "[$session $pane_id] IGNORED: limit text found outside a verified Claude pane."
return 0
fi
if [ -z "$session_list" ]; then
if [ "$(($(date +%s) % 60))" -lt "$CHECK_INTERVAL" ]; then
log "No active tmux sessions found."
fi
reset_epoch=$(parse_reset_time "$pane_content")
if [ -n "$reset_epoch" ] && [ "$reset_epoch" -gt 0 ] 2>/dev/null; then
atomic_write_state "$state_file" rate_limited "$reset_epoch" "$now_epoch" "$pane_id" "$session" "$window_index" "$pane_index"
log "[$session $pane_id] Rate limit detected; exact pane pinned until epoch $reset_epoch."
elif [ "$GUARDIAN_FALLBACK_MODE" = "resume" ]; then
reset_epoch=$((now_epoch + GUARDIAN_FALLBACK_WAIT))
atomic_write_state "$state_file" rate_limited_fallback "$reset_epoch" "$now_epoch" "$pane_id" "$session" "$window_index" "$pane_index"
log "[$session $pane_id] Reset time missing; opt-in fallback scheduled in ${GUARDIAN_FALLBACK_WAIT}s."
else
for session in $session_list; do
# Skip the guardian's own session and control sessions.
# 추가 제외 패턴은 GUARDIAN_SKIP_SESSIONS(공백 구분 glob)로 지정.
case "$session" in
forge-night|claude-retry-*|main) continue ;;
esac
for _skip in ${GUARDIAN_SKIP_SESSIONS:-}; do
case "$session" in $_skip) continue 2 ;; esac
done
log "[$session $pane_id] HOLD: reset time missing; automatic fallback is disabled."
fi
}
if ! pane_runs_claude "$pane_id"; then
log "[$session $pane_id] IGNORED: limit text found outside a verified Claude pane."
return 0
fi
reset_epoch=$(parse_reset_time "$pane_content")
if [ -n "$reset_epoch" ] && [ "$reset_epoch" -gt 0 ] 2>/dev/null; then
atomic_write_state "$state_file" rate_limited "$reset_epoch" "$now_epoch" "$pane_id" "$session" "$window_index" "$pane_index"
log "[$session $pane_id] Rate limit detected; exact pane pinned until epoch $reset_epoch."
elif [ "$GUARDIAN_FALLBACK_MODE" = "resume" ]; then
reset_epoch=$((now_epoch + GUARDIAN_FALLBACK_WAIT))
atomic_write_state "$state_file" rate_limited_fallback "$reset_epoch" "$now_epoch" "$pane_id" "$session" "$window_index" "$pane_index"
log "[$session $pane_id] Reset time missing; opt-in fallback scheduled in ${GUARDIAN_FALLBACK_WAIT}s."
else
if [ "$((now_epoch % 300))" -lt "$CHECK_INTERVAL" ]; then
log "[$session $pane_id] HOLD: reset time missing; automatic fallback is disabled."
fi
fi
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/guardian-watch.sh` around lines 268 - 285, Throttle the “HOLD: reset time
missing” log in the rate-limit handling flow around parse_reset_time and
GUARDIAN_FALLBACK_MODE, using the existing 300-second throttling pattern from
the nearby “Waiting” log. Keep the hold behavior unchanged while preventing the
message from being emitted on every scan.

Comment thread src/guardian-watch.sh
Comment on lines +350 to +358
cleanup_legacy_state() {
local legacy
for legacy in "$MANIFEST_DIR"/session_*.state "$MANIFEST_DIR"/session_*.cooldown; do
[ -e "$legacy" ] || continue
log "Removing legacy session-scoped state: $(basename "$legacy")"
rm -f "$legacy"
done
find "$MANIFEST_DIR" -name 'pane_*.state' -mmin +720 -delete 2>/dev/null || true
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

cleanup_legacy_state가 mtime만으로 유효한 pane 상태를 삭제할 수 있음.

find "$MANIFEST_DIR" -name 'pane_*.state' -mmin +720 -delete는 파일 생성(=detected_epoch) 시점 기준 12시간이 지난 상태 파일을 무조건 삭제합니다. 그런데 GUARDIAN_DETECT_RE(25행)는 weekly 제한도 감지 대상이며, weekly 리셋은 detected_epoch로부터 12시간을 훌쩍 넘길 수 있습니다. 이 함수는 데몬 시작 시 1회 호출(371행)되므로, 라이브 데몬 재시작(테스트 플랜에 명시된 시나리오) 시 아직 유효한 weekly pending 상태가 삭제되고, pane 콘텐츠가 tail -80 밖으로 스크롤된 경우 재감지도 불가능해 자동 재개가 영구히 실패할 수 있습니다. mtime 대신 저장된 reset_epoch 값을 기준으로 만료 여부를 판정하는 것을 권장합니다.

🗑 reset_epoch 기반 정리 제안
 cleanup_legacy_state() {
   local legacy
   for legacy in "$MANIFEST_DIR"/session_*.state "$MANIFEST_DIR"/session_*.cooldown; do
     [ -e "$legacy" ] || continue
     log "Removing legacy session-scoped state: $(basename "$legacy")"
     rm -f "$legacy"
   done
-  find "$MANIFEST_DIR" -name 'pane_*.state' -mmin +720 -delete 2>/dev/null || true
+  local pane_state now_epoch reset_epoch
+  now_epoch=$(date +%s)
+  for pane_state in "$MANIFEST_DIR"/pane_*.state; do
+    [ -e "$pane_state" ] || continue
+    reset_epoch=$(state_value "$pane_state" reset_epoch)
+    if [ -n "$reset_epoch" ] && [ "$now_epoch" -lt "$((reset_epoch + 86400))" ] 2>/dev/null; then
+      continue
+    fi
+    log "Removing stale pane state: $(basename "$pane_state")"
+    rm -f "$pane_state"
+  done
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cleanup_legacy_state() {
local legacy
for legacy in "$MANIFEST_DIR"/session_*.state "$MANIFEST_DIR"/session_*.cooldown; do
[ -e "$legacy" ] || continue
log "Removing legacy session-scoped state: $(basename "$legacy")"
rm -f "$legacy"
done
find "$MANIFEST_DIR" -name 'pane_*.state' -mmin +720 -delete 2>/dev/null || true
}
cleanup_legacy_state() {
local legacy
for legacy in "$MANIFEST_DIR"/session_*.state "$MANIFEST_DIR"/session_*.cooldown; do
[ -e "$legacy" ] || continue
log "Removing legacy session-scoped state: $(basename "$legacy")"
rm -f "$legacy"
done
local pane_state now_epoch reset_epoch
now_epoch=$(date +%s)
for pane_state in "$MANIFEST_DIR"/pane_*.state; do
[ -e "$pane_state" ] || continue
reset_epoch=$(state_value "$pane_state" reset_epoch)
if [ -n "$reset_epoch" ] && [ "$now_epoch" -lt "$((reset_epoch + 86400))" ] 2>/dev/null; then
continue
fi
log "Removing stale pane state: $(basename "$pane_state")"
rm -f "$pane_state"
done
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/guardian-watch.sh` around lines 350 - 358, Update cleanup_legacy_state so
pane_*.state files are removed based on their stored reset_epoch rather than the
file mtime. Parse each state file’s reset_epoch and delete it only when that
reset time has expired, preserving valid pending states such as weekly
restrictions across daemon restarts; retain the existing cleanup for legacy
session files.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant