Skip to content

fix(agent): write context-length cache atomically - #85509

Merged
teknium1 merged 1 commit into
mainfrom
fix/atomic-context-cache-write
Aug 13, 2026
Merged

fix(agent): write context-length cache atomically#85509
teknium1 merged 1 commit into
mainfrom
fix/atomic-context-cache-write

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Summary

The context-length cache was written with a plain truncating open(path, "w") — a crash/kill mid-dump leaves empty or partial YAML, and the next load swallows the parse error and returns {}, silently wiping every persisted context length. Both writers now use the existing atomic_yaml_write() (temp file + fsync + os.replace).

Salvages #40919 by @sasquatch9818 (cherry-picked, authorship preserved), the cleaner of the two duplicate implementations. #35140 by @annguyenNous proposed the same fix 8 days EARLIER — first-submitter credit to @annguyenNous; #40919 was picked for its regression test and comments.

Changes

  • agent/model_metadata.py: save_context_length() + _invalidate_cached_context_length()atomic_yaml_write().
  • tests/agent/test_model_metadata.py: interrupted-write-leaves-cache-intact regression test.

Validation

Result
test_model_metadata.py 75/75 pass
Stale-base gate 0 behind, 2-file diff

Infographic

Atomic cache write — no torn YAML

save_context_length() and _invalidate_cached_context_length() did an
unguarded read-modify-write into $HERMES_HOME/context_length_cache.yaml.
The plain `open(path, "w")` truncates the file before the dump runs. If
the process is killed mid-dump, the file is left empty or partial. The
next _load_context_cache() swallows the YAML error and returns {} —
silently wiping every persisted context length. A concurrent process
reading between truncate and dump-complete also sees a torn file.

After the cache is lost, every model re-probes the network, and when a
probe fails it falls back to the generic 256K default — so a user on a
1M-window model ends up with a wrong, short context window.

Hermes routinely runs several processes against one shared $HERMES_HOME
(a cron agent plus an interactive session, multiple gateway sessions),
so this is hit in normal use.

Switch both writers to the existing utils.atomic_yaml_write helper
(temp file + fsync + os.replace, symlink- and mode-preserving). The real
file is only ever swapped from a fully written temp file, so an
interrupted write leaves the previous cache intact and readers never see
a partial file. Matches the atomic-write pattern already used for
auth.json, config.yaml, and other persisted state.

Makes the persistent model context-length cache write crash-safe. The
old non-atomic write could truncate or wipe the entire cache on an
interrupted or concurrent write, which then forces models onto the wrong
fallback context window. The fix routes both cache writers through the
repo's atomic temp-file + os.replace helper.

N/A

- [x] 🐛 Bug fix (non-breaking change that fixes an issue)
- [ ] ✨ New feature (non-breaking change that adds functionality)
- [ ] 🔒 Security fix
- [ ] 📝 Documentation update
- [ ] ✅ Tests (adding or improving test coverage)
- [ ] ♻️ Refactor (no behavior change)
- [ ] 🎯 New skill (bundled or hub)

- `agent/model_metadata.py`: `save_context_length()` and
  `_invalidate_cached_context_length()` now write via
  `utils.atomic_yaml_write` instead of a truncating `open(path, "w")`.
  Added the `atomic_yaml_write` import.
- `tests/agent/test_model_metadata.py`: added
  `test_write_failure_leaves_existing_cache_intact` — simulates a crash
  during the atomic swap and asserts the existing cache survives
  byte-for-byte with no stray temp file.

1. `pytest tests/agent/test_model_metadata.py -q` — 98 pass, including
   the new crash-safety test.
2. The new test seeds a valid cache, forces the swap step to raise, and
   confirms the file is not truncated and no `.cache_*.tmp` is left.
3. `ruff check agent/model_metadata.py` passes.

- [x] I've read the Contributing Guide
- [x] My commit messages follow Conventional Commits (`fix(scope):`, etc.)
- [x] I searched for existing PRs to make sure this isn't a duplicate
- [x] My PR contains **only** changes related to this fix
- [x] I've run the affected tests (`pytest tests/agent/test_model_metadata.py -q`) and they pass
- [x] I've added tests for my changes
- [x] I've tested on my platform: macOS 15 (Darwin 25.5)

- [x] I've updated relevant documentation (README, `docs/`, docstrings) — or N/A
- [x] I've updated `cli-config.yaml.example` if I added/changed config keys — or N/A
- [x] I've updated `CONTRIBUTING.md` or `AGENTS.md` if I changed architecture or workflows — or N/A
- [x] I've considered cross-platform impact (Windows, macOS) — the helper uses os.replace, which is atomic on both
- [x] I've updated tool descriptions/schemas if I changed tool behavior — or N/A
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on 2c5d428 — fix(models): write context-length cache atomically

⚠️ Warnings

CI timings · View report · View job

Wall time 8m12s vs 5m59s (+37.0%). 10 job(s) slower, 13 faster, 1 unchanged.

  • Python tests / Run tests slice 4/12: +242.0s
  • Python tests / Run tests slice 7/12: -35.0s
  • Python lints / Windows footguns (blocking): -33.0s
  • Python tests / Run tests slice 1/12: +27.0s
  • Python tests / Run tests slice 2/12: +21.0s

OSV vulnerability scan · View job

5 known vulnerabilities found in pinned dependencies.

How to fix:

Review the findings in the Security tab. Update the affected dependencies if a patched version is available.

@teknium1
teknium1 merged commit 6def7ce into main Aug 13, 2026
45 checks passed
@teknium1
teknium1 deleted the fix/atomic-context-cache-write branch August 13, 2026 18:08
@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists labels Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants