diff --git a/CLAUDE.md b/CLAUDE.md index 50a36ed..a0d425c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -232,6 +232,22 @@ rsync -av \ Do not sync Claude Code internal temporary folders. +**The rsync target and the runtime paths are two different locations that can drift.** `rsync` above only updates the git checkout at `~/repo/ba-auto-daily/` on nik-gpu -- it does NOT touch `~/ba_dailies.sh`/`~/ba_daily.py`/`~/ba_auto/`, the actual fixed paths cron (see below) and any manually-run `~/ba_dailies.sh` invocation use. After rsyncing code changes, re-run `./setup.sh` from within `~/repo/ba-auto-daily/` on nik-gpu to propagate them to the runtime paths (it's idempotent -- safe to re-run any time, also re-checks host tools/venv/assets). Confirmed live (2026-07-16): the two locations had drifted (runtime paths were missing a same-day `ba_daily.py` edit) before this was caught and fixed by re-running `setup.sh`. + +### Scheduled runs (cron) + +As of 2026-07-16, `ba_dailies.sh` presets run unattended on a schedule via cron on nik-gpu, not just via manual/CLI invocation. `crontab -l` on nik-gpu is the source of truth; as installed: + +```text +30 3 * * * /home/nik/ba_cron_run.sh daily +30 4 * * * /home/nik/ba_cron_run.sh daily +0 1,5,9,13,17,21 * * * /home/nik/ba_cron_run.sh q4h +``` + +Times are nik-gpu's local system time (`Asia/Tokyo`/JST). `ba_cron_run.sh` (repo root, deployed to `~/ba_cron_run.sh`) wraps `~/ba_dailies.sh ` in a shared, non-blocking `flock` (`~/ba_logs/ba_dailies.lock`) so overlapping fire times can never launch concurrent `xdotool`/`scrot` sessions against the same game window, and logs each run to its own `~/ba_logs/.log` (`daily.log`/`q4h.log`, never interleaved). It contains no game-automation logic -- pure locking/logging plumbing, same category as `ba_dailies.sh` itself. To check whether a scheduled run actually worked: `grep -E 'FAILED|SKIPPED' ~/ba_logs/*.log` -- every run logs one of `OK`/`FAILED (exit N)`/`SKIPPED (previous run still in progress)`, deliberately distinct outcomes (a lock-skip and a genuinely crashed task both exit 1, so the wrapper checks lock acquisition as its own explicit step rather than folding it into the wrapped command's exit code). + +This means: resources (AP, tickets, currency) may already be spent by the time a session starts, independent of anything done in that session -- check `~/ba_logs/` before assuming a given game state is from manual testing. The `daily`/`q4h` preset definitions themselves live in `ba_daily.py`'s `PRESETS` dict (see `plan.md`'s Phase 18 follow-ups for the full writeup), not here -- keep this section in sync if the schedule or preset names change. + ## Runtime dependencies on nik-gpu These are host-level dependencies. Confirm they exist before assuming a bug is in the project code. diff --git a/ba_cron_run.sh b/ba_cron_run.sh new file mode 100644 index 0000000..b5c0233 --- /dev/null +++ b/ba_cron_run.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Cron entry point for ba_dailies.sh presets. Pure process-locking/logging +# plumbing -- no game-automation logic, same "thin wrapper" spirit as +# ba_dailies.sh itself (see CLAUDE.md's Bash policy). +# +# The shared flock lock exists because "daily" fires twice a day (3:30 and +# 4:30) and "q4h" fires every 4 hours -- without a lock, two overlapping +# invocations would both drive xdotool/scrot against the same game window +# at once, which is unsafe. -n (non-blocking): if a previous run is still +# in progress, this invocation is skipped entirely rather than queued, so +# a scheduled fire time can never silently run late. +# +# Logs to ~/ba_logs/.log -- one file per preset, so "daily" and +# "q4h" never interleave. Every line is one of three explicitly-tagged +# outcomes (OK / FAILED / SKIPPED) specifically so a real failure can be +# found with a single command, e.g.: +# grep -E 'FAILED|SKIPPED' ~/ba_logs/*.log +# A bare exit code alone can't do this: flock and a genuinely crashed task +# both exit 1, so lock acquisition is checked as its own explicit step +# (via an fd-based flock, not the "flock -n LOCKFILE COMMAND" form used +# originally) rather than folded into the wrapped command's own exit code. +set -uo pipefail + +PRESET="${1:?usage: ba_cron_run.sh }" +LOG_DIR="$HOME/ba_logs" +LOG_FILE="$LOG_DIR/$PRESET.log" +LOCK_FILE="$LOG_DIR/ba_dailies.lock" +mkdir -p "$LOG_DIR" + +{ + exec 9>"$LOCK_FILE" + if ! flock -n 9; then + echo "=== $(date -Iseconds) SKIPPED $PRESET -- previous run still in progress ===" + exit 0 + fi + + echo "=== $(date -Iseconds) starting $PRESET ===" + "$HOME/ba_dailies.sh" "$PRESET" + status=$? + if [ "$status" -eq 0 ]; then + echo "=== $(date -Iseconds) finished $PRESET OK (exit 0) ===" + else + echo "=== $(date -Iseconds) finished $PRESET FAILED (exit $status) ===" + fi +} >> "$LOG_FILE" 2>&1 diff --git a/plan.md b/plan.md index c58b52c..0b3d8ca 100644 --- a/plan.md +++ b/plan.md @@ -802,6 +802,31 @@ Per CLAUDE.md's Bash policy (`ba_dailies.sh` is a thin launcher only, no loops/s **Verified on nik-gpu without spending anything**: `--list-commands` includes `daily`; requesting a bogus phase name correctly lists both valid phases and valid presets and exits 1. The `daily` preset's actual task sequence (which includes `event_sweep`/`arena`/`lesson`/`bounty`/`shop_common`/`shop_tactical`, all real-resource-spending) has not been run for real yet -- needs explicit user go-ahead before that first live run, per this project's own established live-testing discipline for resource-spending tasks. +The user then added a second preset directly (`"q4h": ["login", "cafe", "mailbox", "event_sweep"]`) and asked for actual `cron` scheduling: `daily` at 3:30 AM and 4:30 AM every day, `q4h` every 4 hours starting at 5:00 AM. + +### Phase 18 follow-up #2: cron scheduling (2026-07-16) + +Deployed a fresh copy first: the repo checkout (`~/repo/ba-auto-daily/`, where this session's rsyncs land) and the "fixed runtime paths" `setup.sh` copies to (`~/ba_dailies.sh`/`~/ba_daily.py`/`~/ba_auto/`, what CLAUDE.md documents as the actual invocation target) had drifted -- the runtime copy predated the user's own `q4h` edit. Re-ran `setup.sh` on nik-gpu to resync them (idempotent, confirmed via `diff -rq`) before pointing cron at `~/ba_dailies.sh`, rather than cron silently running a stale copy. + +Added `ba_cron_run.sh` (repo root, deployed to `~/ba_cron_run.sh`) as the actual cron entry point instead of calling `~/ba_dailies.sh ` directly. It is pure process-locking/logging plumbing -- no game-automation logic, same "thin wrapper" spirit CLAUDE.md's Bash policy already holds `ba_dailies.sh` to. Two things it adds that a bare crontab line can't: + +1. **A shared `flock -n` lock** (`~/ba_logs/ba_dailies.lock`) across every preset invocation. `daily` fires twice an hour apart, and `q4h` fires independently every 4 hours -- without a lock, an overlapping fire time would launch a second concurrent `xdotool`/`scrot` session against the same game window while the first was still mid-navigation, an unsafe race this project has never needed to guard against before now (every previous invocation was a single manual/CLI run). `-n` (non-blocking): if a previous run is still going, the new one is skipped entirely rather than queued -- deliberately, since a queued-and-delayed run could land at an arbitrary later time with no relation to its intended fire time. +2. **Timestamped logging** to `~/ba_logs/.log` (start/finish lines wrapping the actual `ba_dailies.sh` output and exit code), since a cron job's own stdout/stderr otherwise either gets mailed (if a local MTA is configured, not assumed here) or silently dropped. + +**Crontab installed** (`crontab -l` on nik-gpu): +``` +30 3 * * * /home/nik/ba_cron_run.sh daily +30 4 * * * /home/nik/ba_cron_run.sh daily +0 1,5,9,13,17,21 * * * /home/nik/ba_cron_run.sh q4h +``` +Times are nik-gpu's local system time, confirmed `Asia/Tokyo` (JST) via `timedatectl` -- matters since the user's chosen 3:30 fire time isn't arbitrary, it lines up with the reference project's own JP-server restart-check window (`module/restart.py`'s `check_need_restart` checks around hour=3), likely intentional on the user's part to mop up anything left just before the daily reset, with the 4:30 fire catching the freshly-reset dailies an hour later. + +**Verified on nik-gpu without touching the game**: the cron daemon is active (`systemctl is-active cron` -> `active`, `cron` process confirmed running); `ba_cron_run.sh` invoked directly with a bogus preset name correctly logged timestamped start/finish lines wrapping `ba_daily.py`'s own "Unknown phase" error and exit code; a deliberately-forced lock-contention test (holding the lock via a background `flock ... sleep 5`, then invoking the wrapper again) confirmed the second invocation logged start/finish with *no* output in between -- i.e. it correctly skipped running `ba_dailies.sh` at all while the lock was held, rather than racing it. + +**Not yet verified**: an actual scheduled fire (3:30/4:30/the next `q4h` slot) has not yet happened live, so the real end-to-end cron path (crond -> `ba_cron_run.sh` -> `ba_dailies.sh daily`/`q4h` -> real task execution) is confirmed piece-by-piece but not yet as one unattended whole. Also flagged, not fixed (outside this change's scope): whether nik-gpu's GDM desktop session (`DISPLAY=:0`) is guaranteed active at 3:30 AM unattended -- if the machine sleeps, locks in a way that tears down the X session, or otherwise isn't logged in at fire time, `xdotool`/`scrot` would fail and the run would just log an error with nothing to click. Worth checking nik-gpu's power/session settings, or watching the first few real unattended fires' logs, rather than assuming. + +**Follow-up, same day**: per explicit user request ("I also want to have logs for the cron. So I can detect if anythings go wrong. daily and q4h should have separate .log file") -- the per-preset log file separation was already in place (`~/ba_logs/.log`, one file per `$PRESET`), but a real gap was found in the failure-detection half of the ask: a genuinely crashed task and a lock-skip (an overlapping fire time, expected/harmless) both exit 1, and the original wrapper logged only a bare `(exit N)`, making them indistinguishable at a glance. Fixed by switching from the `flock -n LOCKFILE COMMAND` form to an fd-based `flock` (`exec 9>"$LOCK_FILE"; flock -n 9`), checking lock acquisition as its own explicit step separate from the wrapped command's own exit code, so every log line is now tagged with one of three explicit outcomes: `starting`/`finished ... OK (exit 0)`/`finished ... FAILED (exit N)`/`SKIPPED ... -- previous run still in progress` -- a real problem can now be found with `grep -E 'FAILED|SKIPPED' ~/ba_logs/*.log` alone, without having to reason about ambiguous exit codes. **Verified on nik-gpu without touching the game**: a bogus-preset run correctly logged `FAILED (exit 1)`; a deliberately-forced lock-contention run (same technique as the original test) correctly logged `SKIPPED ... -- previous run still in progress` instead of a second ambiguous `(exit 1)`; the grep command above correctly surfaced both lines from `~/ba_logs/smoke_test_bad.log`. Test log removed afterward -- `~/ba_logs/` currently contains no `daily.log`/`q4h.log` yet, since no real scheduled fire has happened. + ## Prerequisites ### OCR