Skip to content

fix: use atomic writes for persistent JSON state files - #27913

Open
vanthinh6886 wants to merge 1 commit into
NousResearch:mainfrom
vanthinh6886:fix/non-atomic-writes-skills-hub
Open

fix: use atomic writes for persistent JSON state files#27913
vanthinh6886 wants to merge 1 commit into
NousResearch:mainfrom
vanthinh6886:fix/non-atomic-writes-skills-hub

Conversation

@vanthinh6886

Copy link
Copy Markdown
Contributor

Problem

Several files write persistent JSON state (installed skills registry, taps config, sticker cache, environment store) using write_text() directly. If the process crashes or loses power mid-write, the file is left in a partially-written state — corrupted JSON that causes JSONDecodeError on next read.

Fix

Replace write_text() with the existing atomic_json_write() helper from utils.py, which uses:

  1. tempfile.mkstemp() — write to a temp file
  2. f.flush() + os.fsync() — ensure data hits disk
  3. os.replace() — atomically swap temp file into place

This is the same pattern already used by gateway/status.py, gateway/pairing.py, and cron/jobs.py.

Files Changed

File What was at risk
tools/skills_hub.py SkillLockFile.save() — installed skills registry
tools/skills_hub.py TapsFile.save() — skill taps configuration
gateway/sticker_cache.py _save_cache() — sticker description cache
tools/environments/base.py _save_json_store() — generic env backend store

Before vs After

Scenario Before After
Crash mid-write Corrupted JSON, JSONDecodeError on next read Previous version intact, no data loss
Normal write Works Works (temp + atomic replace)

Tests

Existing tests cover the happy path. Atomic write correctness is already validated by the atomic_json_write() implementation and its existing test coverage in tests/test_utils.py.

Replace direct write_text() calls with the existing atomic_json_write()
helper (utils.py) which uses temp file + fsync + os.replace to prevent
corruption on crash or power loss.

Files fixed:
- tools/skills_hub.py: SkillLockFile.save() and TapsFile.save()
  (installed skills registry and taps config)
- gateway/sticker_cache.py: _save_cache()
  (sticker description cache)
- tools/environments/base.py: _save_json_store()
  (generic JSON store used by environment backends)
@alt-glitch alt-glitch added type/bug Something isn't working comp/tools Tool registry, model_tools, toolsets comp/gateway Gateway runner, session dispatch, delivery tool/skills Skills system (list, view, manage) duplicate This issue or pull request already exists P3 Low — cosmetic, nice to have labels May 18, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Duplicate of #20532 — same files (skills_hub.py, environments/base.py) and same fix (replace write_text() with atomic_json_write()). #20532 is a superset covering 6 call sites. Also overlaps with #16440 (skills_hub only).

@BoardJames-Bot

Copy link
Copy Markdown

Board James triage pass: this PR's check-attribution failure is the same missing-author-map issue as #27909/#27910: vanthinh6886@gmail.com (vanthinh6886) is not in scripts/release.py AUTHOR_MAP. I opened #27931 with the central fix ("vanthinh6886@gmail.com": "vanthinh6886"). The test job did not expose a branch-local assertion failure in logs; it was cancelled at ~95% progress. After #27931 lands, please rebase/rerun checks.

@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the focused crash-safety fix. I verified the premise still holds on current main for part of the PR, but the salvage needs a little more coverage and one stale hunk handled.

Problems

  • tools/environments/base.py:170, tools/skills_hub.py:3166, and tools/skills_hub.py:3239 still use direct write_text(json.dumps(...)) on current main, so the core fix is still relevant.
  • The same module still has sibling JSON cache/index writes at tools/skills_hub.py:933, tools/skills_hub.py:3126, and tools/skills_hub.py:3563. This matches the duplicate/superset discussion around fix: use atomic writes for persistent state files to prevent corruption #20532, so the current PR is incomplete for the same bug class.
  • gateway/sticker_cache.py:39-56 is already atomic on current main via tempfile.mkstemp, fsync, and os.replace; that hunk is stale rather than newly fixing a current direct write_text call.

Suggested changes

  • When salvaging, convert the remaining skills hub JSON cache/index writes too, or explicitly document why cache corruption is acceptable there.
  • For gateway/sticker_cache.py, either leave the current atomic implementation alone or replace it with atomic_json_write as a cleanup-only change.

Automated hermes-sweeper review.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the focused persistence-safety work. The core premise remains valid on current main, but the patch needs a broader, re-targeted salvage.

Problems

  • tools/environments/base.py:169-172, tools/skills_hub.py:3404-3406, and tools/skills_hub.py:3479-3481 still use direct JSON writes, so those fixes remain useful.
  • The same Skills Hub state module leaves sibling JSON cache writes direct at tools/skills_hub.py:1141-1149, tools/skills_hub.py:3351-3368, and tools/skills_hub.py:3845-3850.
  • gateway/sticker_cache.py:39-56 is already atomic on current main via mkstemp, fsync, and os.replace; that hunk is a shared-helper cleanup rather than a missing crash-safety fix.

Suggested changes

  • Re-target the lock/taps changes to their current locations and include the sibling Skills Hub cache writers, or document why those caches may remain non-atomic.
  • Add save-boundary coverage for the affected helpers; existing helper tests (tests/hermes_cli/test_atomic_json_write.py:13-120) do not verify these call sites are wired to it.

Automated hermes-sweeper review.

Comment thread tools/skills_hub.py
@@ -2594,8 +2595,7 @@ def load(self) -> dict:
return {"version": 1, "installed": {}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please add coverage for this persistence boundary, not only atomic_json_write itself. Current Skills Hub tests exercise save/load behavior but do not verify that the lock and tap writers use the crash-safe write path.

Comment thread gateway/sticker_cache.py
json.dumps(cache, indent=2, ensure_ascii=False),
encoding="utf-8",
)
atomic_json_write(CACHE_PATH, cache)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Current main already writes this cache atomically with mkstemp, fsync, and os.replace; this is a useful consolidation onto the shared helper, but it should be treated as cleanup rather than part of the missing atomic-write fix.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery comp/tools Tool registry, model_tools, toolsets duplicate This issue or pull request already exists P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades tool/skills Skills system (list, view, manage) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants