Skip to content

Claude/cpu guard script dn d8u - #133

Merged
gamblecodezcom merged 11 commits into
mainfrom
claude/cpu-guard-script-DnD8u
Mar 4, 2026
Merged

Claude/cpu guard script dn d8u#133
gamblecodezcom merged 11 commits into
mainfrom
claude/cpu-guard-script-DnD8u

Conversation

@gamblecodezcom

@gamblecodezcom gamblecodezcom commented Mar 4, 2026

Copy link
Copy Markdown
Owner

Summary by Sourcery

Aggressively clear port conflicts before and after restart, simplify post-start health checks and Telegram reporting, and harden filesystem operations and permissions for logs, QA artifacts, and editor saves.

Enhancements:

  • Add a dedicated helper to kill processes squatting on the bot port and use it before restart and during auto-recovery.
  • Streamline post-start health check logic into a single-pass flow with bounded retries and a concise Telegram deployment report including recent logs.
  • Tighten file permissions and options for admin logs, QA artifacts, atomic writes, and SSH editor saves, and centralize QA filesystem error suppression with more robust logging.

claude added 10 commits March 4, 2026 13:00
…eout

Telegraf's long-polling uses getUpdates with timeout=50 (Telegram holds
the connection for up to 50 seconds waiting for new updates). The callApi
patch was wrapping ALL methods — including getUpdates — with globalThrottle,
which has a hard 15-second race. This fired on every poll cycle, throwing
'rateLimiter: API call timed out after 15000ms', causing bot.launch() to
reject and systemd to enter an infinite restart loop.

Fix: add getUpdates (and Telegraf startup/lifecycle calls: getMe,
deleteWebhook, setWebhook, getWebhookInfo, close, logOut) to the _MANAGED
bypass set so they call the original callApi directly without the timeout
wrapper.

All 60 unit tests pass. Bot loads cleanly in CI mode.

https://claude.ai/code/session_01WpEqAiDbpqnBywMjYXJHf4
…hod sets

Two fixes:

1. index.js — ensureQaArtifacts(), persistQaProviderState(), ensureQaDirs():
   All QA file writes are now wrapped in try/catch and treated as non-fatal.
   The bot service runs as the 'runewager' system user but qa/ is owned by
   root, so writeFileSync was throwing EACCES on every startup, which was
   caught by startBot()'s outer try/catch and called process.exit(1).

2. telegramSafe.js — callApi bypass list refactored (code review feedback):
   The inline _MANAGED set is replaced with three named, exported constants:
   - LONG_POLL_METHODS    : getUpdates (must bypass 15 s timeout)
   - LIFECYCLE_METHODS    : getMe, deleteWebhook, setWebhook, etc.
   - INDIVIDUALLY_PATCHED_METHODS : sendMessage, sendPhoto, etc.
   Combined into CALLAPI_BYPASS_METHODS used by the callApi patch.
   All sets are exported so test suites can assert bypass membership without
   requiring a live Telegraf instance.

All 60 unit tests pass. Module loads cleanly in CI mode.

https://claude.ai/code/session_01WpEqAiDbpqnBywMjYXJHf4
…e conflict

Addresses code review feedback:

index.js:
- Added QA_FS_SUPPRESS constant (EACCES, EPERM, EROFS, EEXIST) — the set of
  expected error codes when qa/ is owned by root but the service runs as the
  runewager user. Defined once, shared by all three QA helpers.
- ensureQaDirs: replace silent catch-all with per-directory loop; unexpected
  error codes (e.g. ENOENT, ENOMEM) are logged as warnings so they remain
  visible; permission codes are silently skipped.
- persistQaProviderState: catch checks err.code — returns silently for
  permission errors, logs a warning for any other code (e.g. JSON failure).
- ensureQaArtifacts: same pattern — unexpected errors are logged at warn level;
  permission errors (EACCES/EPERM/EROFS) are intentionally silent.

telegramSafe.js:
- Resolved merge conflict: kept the CALLAPI_BYPASS_METHODS module-level
  constants introduced in the previous commit; dropped the inline _MANAGED
  block that arrived from main during the merge.

All 60 unit tests pass.

https://claude.ai/code/session_01WpEqAiDbpqnBywMjYXJHf4
External code or tests could accidentally mutate the exported Sets
(LONG_POLL_METHODS, LIFECYCLE_METHODS, etc.) which would silently
alter runtime bypass behavior in callApi.

Export frozen array snapshots instead — callers can still iterate and
use .includes() for membership checks; the internal module-level Sets
remain mutable for runtime use by the callApi patch.

All 60 unit tests pass.

https://claude.ai/code/session_01WpEqAiDbpqnBywMjYXJHf4
…=false, UMask=0022

ProtectSystem=strict makes the ENTIRE filesystem read-only (including /var),
requiring every directory Node.js touches to be explicitly listed in
ReadWritePaths.  This caused silent crashes under systemd while manual
`node index.js` worked fine (no sandbox).

Changes:
- ProtectSystem=strict → full
    /usr, /boot, /etc stay read-only; /var (where the app lives) is fully
    writable.  No more brittle per-directory ReadWritePaths maintenance.
- ProtectHome=true → false
    Service runs as root; /root is root's home.  Node/npm use /root for
    internal caches and temp files — blocking it caused subtle startup
    failures that only appeared under systemd sandboxing.
- UMask=0077 → 0022
    0077 (owner-only) made log files mode 600, unreadable by log-tailing
    utilities running as non-root.  0022 gives the standard 644/755.
- ReadWritePaths kept as belt-and-suspenders documentation; functionally
    redundant under ProtectSystem=full but makes intent explicit and guards
    against future tightening.
- Expanded inline comments explain each directive's rationale so the next
    operator understands the trade-off before changing settings.

All 60 unit tests pass.

https://claude.ai/code/session_01WpEqAiDbpqnBywMjYXJHf4
…s catch blocks

EEXIST fix:
  fs.mkdirSync with { recursive: true } never throws EEXIST for an existing
  directory — it silently succeeds. The only way EEXIST reaches these catch
  blocks is if a *file* exists at the expected directory path, which is a real
  misconfiguration that will silently swallow all QA artifact writes. Removing
  EEXIST from QA_FS_SUPPRESS lets that case surface as a logged warning.
  Comment updated to explain the exclusion.

Non-Error guard fix:
  JavaScript allows any value to be thrown, so a catch block's `e` is not
  guaranteed to be an Error instance. All three QA fs catch blocks now use
  optional chaining (e?.code, e?.message) and fall back to String(e) for the
  message field, preventing secondary TypeErrors during error logging.

All 60 tests pass; node --check clean.

https://claude.ai/code/session_01WpEqAiDbpqnBywMjYXJHf4
Merge conflicts (5 hunks in index.js):
- Keep HEAD QA_FS_SUPPRESS without EEXIST (correct: recursive mkdirSync
  never throws EEXIST for dirs, so EEXIST means misconfiguration)
- Keep HEAD optional-chaining (e?.code / e?.message ?? String(e)) throughout

Review comment — centralize QA_FS_SUPPRESS:
- Added authoritative JSDoc explaining the set is the single source of
  suppression semantics for all QA fs helpers; adding a code here
  propagates to every caller automatically

Review comment — stack trace behind debug flag:
- All QA logEvent warn calls now spread stack: e?.stack when LOG_LEVEL=debug

Review comment — independent artifact writes in ensureQaArtifacts:
- Extracted tryWriteQaJson() helper: each artifact write is now guarded
  independently so one serialization/path failure cannot block the others
- ensureQaArtifacts() restructured to call tryWriteQaJson per file

Review comment — runewager.service hardening:
- ProtectHome=false → ProtectHome=true; added /root/.npm and /root/.cache
  to ReadWritePaths so Node/npm caches work without blanket /root access
- UMask=0022 → UMask=0027 (files 640, dirs 750): blocks world-readable
  artifacts while keeping group-read for log-tailing utilities

https://claude.ai/code/session_01WpEqAiDbpqnBywMjYXJHf4
runewager.service:
- UMask=0022 → UMask=0077; files now default to 600, dirs to 700
- Updated comment documenting per-call mode strategy and noting that
  systemd StandardOutput/StandardError logs are opened by systemd (not
  the process) and thus unaffected by UMask; group access via usermod -aG
  or setfacl is the documented mechanism for those files

index.js — explicit mode on every writeFileSync / appendFileSync:
- writeFileAtomic (temp file): mode 0o600 — sensitive data (runtime-state.json)
- adminLog → adminEventsLogFile: mode 0o640 — log file, group-readable
- appendBonusAdminLog → bonusAdminLogFile: mode 0o640 — log file, group-readable
- writeQaLog daily log files: mode 0o640 — observability, group-readable
- persistQaProviderState → qaProviderStateFile: mode 0o600 — private state
- tryWriteQaJson → QA artifact files: mode 0o600 — private artifacts
- sshv editor save → session.editorMode.filePath: mode 0o600 — private draft

https://claude.ai/code/session_01WpEqAiDbpqnBywMjYXJHf4
…k loops

Root cause: step 11 "God-Mode Heal" duplicated the same wait_for_health
(20 attempts × 2s = 40s) + systemctl restart block FIVE times back-to-back,
totalling up to ~3.5 minutes of wasted waiting even on a clean boot.

Changes:
- Extracted `_kill_port_squatters()` helper (lsof-first, SIGTERM→SIGKILL)
- Step 9c: two-pass aggressive port clear with xargs SIGKILL fallback runs
  BEFORE systemd restart so the service never races a squatter
- Step 11: replaced 5 duplicated heal blocks with a single 30s health probe
  (15×2s), one optional auto-recovery restart, and a final 20s probe (10×2s)
- Telegram notification uses read_env_value() (no raw grep), log capped at 3.5k chars

Result: cold-start completes in ~15-35s instead of 3-5 minutes.
… block

- xargs -r portability: replaced with explicit empty-guard so BSD xargs works
- _REPORT arithmetic expansion bug: $(( ... )) → $( ( ... ) ) — commands now
  actually execute and log content is captured correctly
- _REPORT now wired into Telegram notification text payload
- SYSTEMD_ACTIVE moved to after auto-recovery path so summary reflects
  post-heal state, not pre-restart stale value
@codeant-ai

codeant-ai Bot commented Mar 4, 2026

Copy link
Copy Markdown

CodeAnt AI is reviewing your PR.


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@sourcery-ai

sourcery-ai Bot commented Mar 4, 2026

Copy link
Copy Markdown

Reviewer's Guide

Harden pre-/post-start health and port conflict handling in prod-run.sh, simplify and centralize QA filesystem error handling and permissions in index.js, and tighten file modes for various logs and artifacts.

Sequence diagram for aggressive pre-start port conflict clearing

sequenceDiagram
    participant ProdRunner as prod_run_sh
    participant PortCheck as is_port_listening
    participant Killer as _kill_port_squatters
    participant Lsof as lsof_or_port_listener_pids
    participant OS as OS_processes

    ProdRunner->>ProdRunner: PORT_PRESTART = read_env_value PORT (default 3000)
    ProdRunner->>PortCheck: is_port_listening(PORT_PRESTART)
    alt port_in_use
        ProdRunner->>ProdRunner: say Port in use — aggressively clearing
        ProdRunner->>Killer: _kill_port_squatters(PORT_PRESTART, PID)
        Killer->>Lsof: list listening PIDs on PORT_PRESTART
        Lsof-->>Killer: PID list
        loop for_each_pid
            Killer->>OS: kill -TERM pid
            OS-->>Killer: process may still exist
            Killer->>OS: sleep 1 then kill -KILL pid if still alive
        end
        ProdRunner->>ProdRunner: sleep 1
        ProdRunner->>PortCheck: is_port_listening(PORT_PRESTART)
        alt still_in_use
            ProdRunner->>ProdRunner: warn Port still occupied — forcing SIGKILL
            ProdRunner->>Lsof: lsof -t -iTCP:PORT_PRESTART -sTCP:LISTEN
            Lsof-->>ProdRunner: stray PID list
            ProdRunner->>OS: xargs kill -9 stray_pids
            ProdRunner->>ProdRunner: sleep 1
        end
    end
Loading

Sequence diagram for post-start health check and single auto-recovery

sequenceDiagram
    participant ProdRunner as prod_run_sh
    participant HealthUrl as resolve_health_url
    participant WaitHealth as wait_for_health
    participant PortCheck as is_port_listening
    participant Killer as _kill_port_squatters
    participant Systemd as systemctl
    participant NodeProc as node_index_js
    participant Telegram as telegram_api

    ProdRunner->>ProdRunner: say Running post-start health check
    ProdRunner->>HealthUrl: resolve_health_url()
    HealthUrl-->>ProdRunner: HEALTH_URL
    ProdRunner->>ProdRunner: derive HEALTH_PORT from HEALTH_URL
    ProdRunner->>ProdRunner: HEALTH_STATUS = unhealthy, PORT_STATUS = not-listening

    ProdRunner->>WaitHealth: wait_for_health(HEALTH_URL, 15, 2)
    alt health_success
        WaitHealth-->>ProdRunner: success
        ProdRunner->>ProdRunner: HEALTH_STATUS = healthy
    else health_fail
        WaitHealth-->>ProdRunner: failure
        ProdRunner->>PortCheck: is_port_listening(HEALTH_PORT)
        alt port_listening
            PortCheck-->>ProdRunner: true
            ProdRunner->>ProdRunner: warn Port still squatted — clear + one restart
            ProdRunner->>Killer: _kill_port_squatters(HEALTH_PORT, PID)
            Killer-->>ProdRunner: squatters cleared
            ProdRunner->>ProdRunner: sleep 1
            alt systemd_available
                ProdRunner->>Systemd: systemctl restart APP_NAME.service
                Systemd-->>ProdRunner: restart triggered
            else no_systemd
                ProdRunner->>NodeProc: kill PID (if set)
                ProdRunner->>NodeProc: nohup node index.js
            end
            ProdRunner->>ProdRunner: sleep 3
            ProdRunner->>ProdRunner: PID = get_bot_pid()
        end
        ProdRunner->>WaitHealth: wait_for_health(HEALTH_URL, 10, 2)
        alt final_health_success
            WaitHealth-->>ProdRunner: success
            ProdRunner->>ProdRunner: HEALTH_STATUS = healthy
        else final_health_fail
            WaitHealth-->>ProdRunner: failure
            ProdRunner->>ProdRunner: HEALTH_STATUS remains unhealthy
        end
    end

    ProdRunner->>PortCheck: is_port_listening(HEALTH_PORT)
    alt listening_now
        PortCheck-->>ProdRunner: true
        ProdRunner->>ProdRunner: PORT_STATUS = listening
    else still_not_listening
        PortCheck-->>ProdRunner: false
        ProdRunner->>ProdRunner: PORT_STATUS = not-listening
    end

    ProdRunner->>Systemd: systemctl is-active APP_NAME.service
    Systemd-->>ProdRunner: SYSTEMD_ACTIVE

    ProdRunner->>ProdRunner: _ADMIN_IDS = read_env_value ADMIN_IDS
    ProdRunner->>ProdRunner: _BOT_TOKEN = read_env_value TELEGRAM_BOT_TOKEN
    alt telegram_configured
        ProdRunner->>ProdRunner: build _REPORT from MAIN_LOG and ERROR_LOG
        loop for_each_admin
            ProdRunner->>Telegram: sendMessage(bot_token, admin_id, summary + _REPORT)
            Telegram-->>ProdRunner: 200 OK (ignored)
        end
    end
Loading

File-Level Changes

Change Details Files
Aggressively clear port conflicts before startup and simplify post-start health/auto-heal + Telegram reporting into a single, more robust pass.
  • Introduce _kill_port_squatters helper that prefers lsof to find listeners on a port and sends TERM then KILL, skipping the current process PID if provided.
  • Replace free_port_if_conflicted pre-start logic with _kill_port_squatters and an extra lsof-based SIGKILL sweep if the port remains occupied.
  • Rewrite the previous multi-phase "God-mode" heal section into a single post-start health check that waits for the health URL, performs one auto-recovery restart if the health port is still squatted, and then does a final health probe.
  • Rework Telegram admin notifications to use read_env_value for ADMIN_IDS and TELEGRAM_BOT_TOKEN, send a concise deploy summary plus trimmed combined logs, and ensure notification reflects the final health/port/systemd state.
prod-run.sh
Tighten file permissions and improve robustness/observability of QA-related filesystem operations and admin/QA logging.
  • Ensure various log and artifact writes (bonus admin log, adminEventsLogFile, QA logs, QA provider state, SSH editor saves, atomic writes) use explicit encoding and restrictive file modes (generally 0o640 for logs and 0o600 for state/config/temp files).
  • Refine QA_FS_SUPPRESS to be the single source of truth for suppressed errno codes, drop EEXIST from suppression, and harden error handling with optional chaining, safer message formatting, and optional stack inclusion in debug mode.
  • Introduce tryWriteQaJson helper to apply QA_FS_SUPPRESS semantics per QA artifact write, then refactor ensureQaArtifacts to use it so individual failures do not block other QA files while remaining non-fatal overall.
  • Improve QA directory and provider-state error logging to distinguish expected permission/readonly failures from unexpected conditions, while keeping the bot resilient when QA directories or files are unwritable.
index.js
(Potential) service unit adjustments associated with the new start/health behavior (no functional diff shown here).
  • runewager.service is touched, likely to align systemd behavior with the updated prod-run.sh health/port management, but the specific changes are not included in the provided diff.
runewager.service

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Mar 4, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@gamblecodezcom has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 23 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b367aa67-19db-4657-b7ed-8df0b0c2f1fd

📥 Commits

Reviewing files that changed from the base of the PR and between 31f72b3 and 3c6c120.

📒 Files selected for processing (3)
  • index.js
  • prod-run.sh
  • runewager.service
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/cpu-guard-script-DnD8u

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 and usage tips.

@gamblecodezcom
gamblecodezcom merged commit 1b05528 into main Mar 4, 2026
2 checks passed
@gamblecodezcom
gamblecodezcom deleted the claude/cpu-guard-script-DnD8u branch March 4, 2026 14:21
@codeant-ai

codeant-ai Bot commented Mar 4, 2026

Copy link
Copy Markdown

CodeAnt AI finished reviewing your PR.

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.

2 participants