Skip to content

fix: stop ops-post-update-migrate self-rsync load storm on SessionStart#659

Merged
Aurora Capital (auroracapital) merged 1 commit into
mainfrom
fix/ops-post-update-migrate-self-rsync-load-storm
Jul 6, 2026
Merged

fix: stop ops-post-update-migrate self-rsync load storm on SessionStart#659
Aurora Capital (auroracapital) merged 1 commit into
mainfrom
fix/ops-post-update-migrate-self-rsync-load-storm

Conversation

@auroracapital

@auroracapital Aurora Capital (auroracapital) commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Problem

Host load spiked to 130+ on a 16-core Mac. Root cause traced to a self-referential rsync storm from this plugin's bin/ops-post-update-migrate.

refresh_current_directory() runs unconditionally on every SessionStart (line 68, before the sentinel check). It computes current_dir = dirname(PLUGIN_ROOT)/current. But the SessionStart hook invokes the script from .../ops/current/, so PLUGIN_ROOT is current/ → the command becomes:

rsync -a --delete .../ops/current/  .../ops/current/    # SRC == DST

Copying a directory onto itself with --delete. With ~10 background Claude sessions each firing SessionStart (plus ops-daemon-manager.sh ensure-current also spawning it), the self-rsyncs stack and never converge. The 60s watchdog only wraps run_migrations, not refresh_current_directory, so each ran unbounded — observed 14–17 minutes alive — hammering the disk and pinning fseventsd at ~55%, cascading into secd/trustd/cloudd.

Observed live: 8+ concurrent rsync -a --delete .../current/ .../current/ pairs.

Fix

  • Same-path guard: skip the rsync/cp entirely when realpath(PLUGIN_ROOT) == realpath(current_dir) — the common case, where you already are current/ and there is nothing to copy.
  • Single-flight lock: mkdir-based lock so concurrent sessions can't stack refreshes, with a 2-minute stale-lock sweep for killed runs.
  • Same guard applied to the duplicate rsync block inside run_migrations (feat(slack): scan Slack Desktop, Firefox, Safari for cookies #4).

Verification

  • bash -n clean.
  • Running the script exactly as the SessionStart hook does (CLAUDE_PLUGIN_ROOT=.../ops/current) now logs skip: PLUGIN_ROOT already == current/ — no self-rsync and spawns zero rsyncs.
  • Live host load recovered from 130+ → downtrend (1-min < 15-min) after killing the storm and hot-patching the cached copy; this PR lands the same fix in canonical so it survives plugin updates.

🤖 Generated with Claude Code


Note

Low Risk
Targeted guard and locking around an existing refresh path; behavior when plugin root differs from current/ is unchanged aside from serialized refreshes.

Overview
Fixes a host load spike caused by refresh_current_directory() running on every SessionStart while the hook’s PLUGIN_ROOT is already .../ops/current/, so rsync -a --delete copied that directory onto itself and stacked across many sessions.

refresh_current_directory() now compares canonical paths (pwd -P) and skips rsync/cp when source and destination are the same. When a real refresh is needed, a mkdir-based single-flight lock (with a 2-minute stale lock cleanup) ensures only one rsync runs at a time; other processes log and skip.

The duplicate migration step #4 current/ sync gets the same-path skip only (no lock in that block), so the common “already on current/” case no longer spawns rsync there either.

Reviewed by Cursor Bugbot for commit f8b8623. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • Bug Fixes
    • Prevented unnecessary refresh operations when the active plugin directory already matches the target directory.
    • Reduced duplicate refresh activity by avoiding overlapping update steps when multiple hooks run at the same time.
    • Kept existing update behavior intact, including fallback copy handling and post-refresh plugin metadata updates.

refresh_current_directory() runs unconditionally on every SessionStart
(before the sentinel check). When the plugin already runs FROM current/,
PLUGIN_ROOT resolves to current/ and current_dir = dirname(PLUGIN_ROOT)/current
== current/ — so `rsync -a --delete SRC/ SRC/` copies the dir onto itself.

With ~10 background Claude sessions each firing SessionStart (plus
ops-daemon-manager ensure-current), these self-rsyncs stack and never
converge. The 60s timeout only wraps run_migrations, not
refresh_current_directory, so each ran unbounded (observed 14-17 min alive),
hammering the disk and pinning fseventsd — driving host load to 130+.

Fix:
- Skip the rsync/cp entirely when realpath(PLUGIN_ROOT) == realpath(current_dir)
  (the common case — no work to do, you ARE current/).
- Single-flight mkdir lock so concurrent sessions can't stack refreshes, with
  a 2-min stale-lock sweep for killed runs.
- Same guard applied to the duplicate block in run_migrations (#4).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@claude claude 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.

⚠️ Code review skipped — your organization's overage spend limit has been reached.

Code review is billed via overage credits. To resume reviews, an organization admin can raise the monthly limit at claude.ai/admin-settings/claude-code.

Once credits are available, push a new commit or reopen this pull request to trigger a review.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

[[ -d "$lock" ]] && [[ -n "$(find "$lock" -maxdepth 0 -mmin +2 2>/dev/null)" ]] \
&& rmdir "$lock" 2>/dev/null || true # clear stale lock from a killed run
if mkdir "$lock" 2>/dev/null; then
trap 'rmdir "'"$lock"'" 2>/dev/null || true' RETURN

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: The trap ... RETURN on line 51 will not clean up the lock directory if the script is terminated by a signal, causing subsequent operations to be skipped.
Severity: HIGH

Suggested Fix

Replace the trap ... RETURN inside the function with a script-wide trap ... EXIT handler. An EXIT trap executes on any script exit, including normal termination and termination via most signals (like SIGTERM), ensuring the lock directory is reliably removed.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: claude-ops/bin/ops-post-update-migrate#L51

Potential issue: The `refresh_current_directory` function in `ops-post-update-migrate`
uses `trap ... RETURN` to clean up a lock directory. This script is executed as an
asynchronous `SessionStart` hook, which can be terminated with `SIGTERM` by the parent
process if it runs too long. A `RETURN` trap only fires on a normal function return, not
when the script is terminated by a signal. Consequently, if the script is killed while
the lock is held, the lock directory will persist. Subsequent script executions within
the next 2 minutes will detect the stale lock and skip the directory refresh, leading to
the `current/` directory becoming out of sync with the active plugin version.

Did we get this right? 👍 / 👎 to inform future reviews.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a real-path comparison guard to skip redundant rsync/cp refreshes when PLUGIN_ROOT already points to the current/ directory, and introduces a single-flight filesystem lock around the refresh logic in refresh_current_directory to prevent concurrent SessionStart hooks from stacking operations.

Changes

Self-refresh Guard and Locking

Layer / File(s) Summary
Real-path guard and lock in refresh function
claude-ops/bin/ops-post-update-migrate
Resolves real paths for PLUGIN_ROOT and current_dir, skips refresh when equal, and wraps rsync/cp refresh in a single-flight lock directory with RETURN trap cleanup and skip logging.
Real-path guard in run_migrations
claude-ops/bin/ops-post-update-migrate
Applies the same real-path comparison in the "Stable current/ directory" block to skip refresh when paths match, otherwise proceeds with existing rsync/cp logic.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main fix: preventing self-rsync load storms during SessionStart.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ops-post-update-migrate-self-rsync-load-storm

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.

@auroracapital Aurora Capital (auroracapital) merged commit dd0b757 into main Jul 6, 2026
18 of 19 checks passed
@auroracapital Aurora Capital (auroracapital) deleted the fix/ops-post-update-migrate-self-rsync-load-storm branch July 6, 2026 12:24

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is ON, but it could not run because the branch was deleted or merged before autofix could start.

Reviewed by Cursor Bugbot for commit f8b8623. Configure here.

[[ -d "$lock" ]] && [[ -n "$(find "$lock" -maxdepth 0 -mmin +2 2>/dev/null)" ]] \
&& rmdir "$lock" 2>/dev/null || true # clear stale lock from a killed run
if mkdir "$lock" 2>/dev/null; then
trap 'rmdir "'"$lock"'" 2>/dev/null || true' RETURN

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

RETURN trap clears peer lock

Medium Severity

The two-minute stale sweep can remove .refresh-current.lock while another session is still refreshing. A later session may mkdir the same lock path and run rsync. When the original session’s refresh_current_directory returns, its RETURN trap always rmdirs that path, dropping the active holder’s lock and weakening single-flight protection under concurrent SessionStart hooks.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f8b8623. Configure here.

@cursor cursor 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.

Not approving: Cursor Bugbot reported 1 unresolved medium-severity finding (RETURN trap may clear a peer refresh lock), and the Bugbot check finished as skipped. Human review is needed before merge; no reviewers were assigned because the only repo collaborator is the PR author.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
claude-ops/bin/ops-post-update-migrate (1)

149-167: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

claude-ops/bin/ops-post-update-migrate:149-167 — Reuse the locked refresh helper here

This duplicated current/ refresh still runs without the single-flight lock, so version-bump sessions can stack refresh I/O and keep the load-storm fix half-applied. Call refresh_current_directory() here instead of maintaining a second copy.

🤖 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 `@claude-ops/bin/ops-post-update-migrate` around lines 149 - 167, The
duplicated current/ refresh logic in ops-post-update-migrate is bypassing the
single-flight lock, which can still allow concurrent refresh I/O. Replace the
inline PLUGIN_ROOT-to-current copy/sync block with a call to
refresh_current_directory() so this path uses the same locked helper as the
version-bump flow and avoids maintaining a second refresh implementation.
🤖 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.

Outside diff comments:
In `@claude-ops/bin/ops-post-update-migrate`:
- Around line 149-167: The duplicated current/ refresh logic in
ops-post-update-migrate is bypassing the single-flight lock, which can still
allow concurrent refresh I/O. Replace the inline PLUGIN_ROOT-to-current
copy/sync block with a call to refresh_current_directory() so this path uses the
same locked helper as the version-bump flow and avoids maintaining a second
refresh implementation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 493abd62-3184-471a-be09-a331f5e45c63

📥 Commits

Reviewing files that changed from the base of the PR and between 0122411 and f8b8623.

📒 Files selected for processing (1)
  • claude-ops/bin/ops-post-update-migrate

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