# NousResearch/hermes-agent · File sync rate-limit clock advances on error

## Open now

- Nothing open on GitHub.

These issues and PRs address a bug where an unsuccessful file sync cycle incorrectly advances the rate-limit clock, suppressing the expected immediate retry. Multiple fix attempts and related salvage PRs are present.

## TL;DR

- 7 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 5 PR(s) closed without merging.
- A fix was merged to main in PR #67927. Verify it addresses your case.
- **Recommended:** read PR #67927 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-67927`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.29`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #39945: FileSyncManager: a failed sync() advances the rate-limit clock, suppressing the documented retry

## Prior & competing PRs

- **#67927: fix(file-sync): don't rate-limit retry after a failed sync cycle (salvage #39946)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `tests/tools/test_file_sync.py`, `tools/environments/file_sync.py`
  - fix hunks:
  - `tests/tools/test_file_sync.py`

    ```diff
    @@ -225,6 +225,42 @@ def test_env_var_forces_sync(self, tmp_files, tmp_path):
                 mgr.sync()
             assert upload.call_count == 1
     
    +    def test_failed_sync_does_not_suppress_next_retry(self, tmp_files, monkeypatch):
    +        """A failed sync must not advance the rate-limit clock.
    +
    +        Regression: the failure path used to set ``_last_sync_time`` on
    +        rollback, so the next non-forced ``sync()`` within ``sync_interval``
    +        hit the rate-limit guard and returned early — silently suppressing the
    +        retry the rollback had just prepared and leaving the remote stale.
    +        """
    +        from tools.environments import file_sync
    +
    +        clock = {"t": 1000.0}
    +        monkeypatch.setattr(file_sync, "_monotonic", lambda: clock["t"])
    +
    +        upload = MagicMock(side_effect=RuntimeError("transport down"))
    +        mgr = FileSyncManager(
    +            get_files_fn=_make_get_files(tmp_files),
    +            upload_fn=upload,
    +            delete_fn=MagicMock(),
    +            sync_interval=10.0,
    +        )
    +
    +        # First sync fails (forced bypasses the guard); state rolls back.
    +        mgr.sync(force=True)
    +        assert upload.call_count >= 1
    +
    +        # Transport recovers; advance the clock by LESS than the interval.
    +        upload.reset_mock()
    +        upload.side_effect = None
    +        clock["t"] = 1002.0  # 2s later, < 10s interval
    +
    +        # The next non-forced cycle must retry, not be rate-limited away.
    +        mgr.sync()
    +        assert upload.call_count == 3, (
    +            "a failed sync must not rate-limit the next retry"
    +        )
    +
     
     class TestEdgeCases:
         def test_empty_file_list(self):
    ```
  - `tools/environments/file_sync.py`

    ```diff
    @@ -35,6 +35,9 @@
     # ``time.sleep`` globally because ``time`` is the module object; under xdist
     # that lets unrelated background threads inflate retry-test call counts.
     _sleep = time.sleep
    +# Same rationale for the rate-limit clock: tests patch ``_monotonic``
    +# instead of ``time.monotonic`` on the shared module object.
    +_monotonic = time.monotonic
     
     _SYNC_INTERVAL_SECONDS = 5.0
     _FORCE_SYNC_ENV = "HERMES_FORCE_FILE_SYNC"
    ```

    ```diff
    @@ -169,7 +172,7 @@ def sync(self, *, force: bool = False) -> None:
             On failure, state rolls back so the next cycle retries everything.
             """
             if not force and not os.environ.get(_FORCE_SYNC_ENV):
    -            now = time.monotonic()
    +            now = _monotonic()
                 if now - self._last_sync_time < self._sync_interval:
                     return
     
    ```

    ```diff
    @@ -193,7 +196,7 @@ def sync(self, *, force: bool = False) -> None:
             to_delete = [p for p in self._synced_files if p not in current_remote_paths]
     
             if not to_upload and not to_delete:
    -            self._last_sync_time = time.monotonic()
    +            self._last_sync_time = _monotonic()
                 return
     
             # Snapshot for rollback (only when there's work to do)
    ```

    ```diff
    @@ -227,12 +230,17 @@ def sync(self, *, force: bool = False) -> None:
                     self._pushed_hashes.pop(p, None)
     
                 self._synced_files = new_files
    -            self._last_sync_time = time.monotonic()
    +            self._last_sync_time = _monotonic()
     
             except Exception as exc:
                 self._synced_files = prev_files
                 self._pushed_hashes = prev_hashes
    -            self._last_sync_time = time.monotonic()
    +            # Do NOT advance _last_sync_time here: a failed cycle rolls state
    +            # back so the next cycle can retry. Bumping the rate-limit clock on
    +            # failure would make the next non-forced sync() return early (the
    +            # guard above), suppressing that retry for up to _sync_interval and
    +            # leaving the remote with stale files — contradicting this method's
    +            # documented "next cycle retries everything" contract.
                 logger.warning("file_sync: sync failed, rolled back state: %s", exc)
     
         # ------------------------------------------------------------------
    ```
- **#39946: fix(file-sync): don't rate-limit retry after a failed sync cycle**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#67927`, `#39945`
- **#39952: fix(file-sync): retry immediately after failed sync**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#39945`
- **#39958: fix(file-sync): don't advance rate-limit clock on failed sync()**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#67927`, `#39946`
- **#40040: fix(file-sync): retry immediately after failed sync**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#67927`, `#39946`, `#39945`
- **#40048: fix(file_sync): don't advance rate-limit clock on failed sync cycle**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#39946`, `#39945`, `#39958`, `#40040`

## Fix status

- Merged to main in PR #67927 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-20`.
- Subsystem: `environments`
