Skip to content

feat: per-skill file lock for cross-process write safety #71091

Description

@jasstionzyf

Problem

Under concurrent multi-user load (gateway with many active sessions), multiple processes/threads within the same profile can write to skill files simultaneously with no cross-process synchronization:

Writer Path Scenario
skill_manage tool tools/skill_manager_tool.py Agent-initiated or background review daemon
Curator agent/curator.py Auto-archive/delete stale skills
CLI hermes skills install/update/uninstall Manual operations
Background review daemon thread → forked AIAgent → skill_manage Triggers every ~10 turns per session; multiple sessions may fire concurrently

None of these paths hold any cross-process lock. A background review in session A writing skill_manage(action='patch', ...) races with curator archiving the same skill in another thread, or two background reviews from different sessions patching the same SKILL.md simultaneously.

Additionally, the terminal tool's cwd state is shared across sessions (tools/terminal_tool.py line 1076: "the terminal env is shared across sessions").

Proposed Solution: Per-Skill File Lock

Use fcntl.flock() on the skill directory FD (not a separate lock file), scoped per skill name so writes to different skills are fully concurrent:

write "python-tips"  →  flock(skills/python-tips/)    ← only this skill locked
write "git-workflow" →  flock(skills/git-workflow/)   ← no wait for python-tips
create new skill     →  flock(skills/)                ← parent-dir lock to prevent
                                                         duplicate names

Why directory FD instead of a lock file

  • No extra file to create / clean up / manage
  • fcntl.flock() on a directory fd works identically to file fd on Linux/macOS
  • Kernel-managed: released on process death, no stale lock problem
  • The directory is the natural serialization boundary — one skill = one dir

API

# agent/skill_lock.py
from contextlib import contextmanager

@contextmanager
def skill_write_lock(skill_name: str, timeout: float = 30):
    """Acquire a per-skill write lock, cross-process safe."""
    skill_dir = Path(get_hermes_home()) / "skills" / skill_name
    target = skill_dir if skill_dir.is_dir() else skill_dir.parent
    fd = os.open(str(target), os.O_RDONLY)
    try:
        _flock_wait(fd, timeout)
        yield
    finally:
        fcntl.flock(fd, fcntl.LOCK_UN)
        os.close(fd)

Integration points (3 files)

  1. tools/skill_manager_tool.py — wrap all 6 actions (create/edit/patch/delete/write_file/remove_file)
  2. agent/curator.py — wrap archive/delete operations
  3. hermes_cli/skills.py — wrap install/update/uninstall

Delete handling

Before deleting a skill directory, os.rename to a temp location first, then shutil.rmtree — releases lock before slow deletion.

Why reads don't need the lock

Skill writes already use atomic write temp + rename internally. Readers always see either the old file or the new file, never a partial write. Curator moves are handled by the delete rename pattern above.

Windows fallback

Use msvcrt.locking() + PID-based lock file with stale detection.

Scope

  • New file: agent/skill_lock.py (~60 lines)
  • Modified: 3 existing files
  • Tests: tests/agent/test_skill_lock.py

Notes

The same profile-level sharing problem also applies to terminal cwd state — tracked separately in tools/terminal_tool.py's ongoing "cwd rearchitecture" (step 1 in progress). This issue focuses on skills only.

If maintainers are interested, I'm willing to submit a PR implementing this plan.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Low — cosmetic, nice to havecomp/agentCore agent runtime: loop, agent_init, prompt builder, context-compression, responses endpointneeds-decisionAwaiting maintainer decision before any implementationtool/skillsSkills system (list, view, manage)type/featureNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions