Skip to content

media-use prefs.mjs: parallel record calls fail with ENOENT on a shared preferences.json.tmp and lose keys #5333

Closed

Description

@thegreatLUCY

Describe the bug

writePrefsFile() in skills/media-use/scripts/lib/prefs-store.mjs writes atomically, through a temporary file and then a rename. But the temporary name is fixed:

const tmp = `${path}.tmp`;
writeFileSync(tmp, `${JSON.stringify(file, null, 2)}\n`);
renameSync(tmp, path);

When two prefs.mjs record processes run at the same time, they share preferences.json.tmp. The first rename moves the file away, and the second rename fails with ENOENT.

There is a second, related problem. record reads the whole file, changes one key and writes the whole file back, so concurrent calls also overwrite each other's keys. A call that exits 0 can still lose its value.

This is easy to hit from the documented workflow. faceless-explainer Step 0 asks the agent to record each preference-backed answer with prefs.mjs record, and an agent following it ran those calls in parallel (one per key).

Steps to reproduce

MEDIA=<path to skills/media-use>
mkdir /tmp/prefs-repro && cd /tmp/prefs-repro && echo '{}' > hyperframes.json
export HYPERFRAMES_MEDIA_HOME=/tmp/prefs-repro/home   # keeps the real ~/.media out of the test
node $MEDIA/scripts/prefs.mjs record --hyperframes . --key destination --value youtube-shorts &
node $MEDIA/scripts/prefs.mjs record --hyperframes . --key aspect --value 1080x1920 &
node $MEDIA/scripts/prefs.mjs record --hyperframes . --key language --value en &
node $MEDIA/scripts/prefs.mjs record --hyperframes . --key flow --value automation &
wait; cat .media/preferences.json

Expected behavior

All four record calls succeed, and .media/preferences.json contains all four keys.

Actual behavior

Over 3 runs, 2–3 of the 4 calls exited with:

ENOENT: no such file or directory, rename '<project>/.media/preferences.json.tmp' -> '<project>/.media/preferences.json'

Only 0–2 of the 4 keys were kept (run 1: none; run 2: destination; run 3: flow, aspect).

Suggested fix

  1. Use a unique temporary name, as scripts/lib/telemetry.mjs already does: ${path}.${process.pid}.tmp. manifest.mjs uses random bytes for the same purpose. This removes the ENOENT.
  2. To also stop lost updates, serialise the read-modify-write in recordPreference(). For example, take a lock with mkdirSync(${path}.lock) and a short retry loop, then re-read the file under the lock before writing.

Alternatively, or as well, Step 0 of the workflow skills could say that record calls must run one after another.

I'm happy to open a PR for (1) and (2) if that would help.

Environment

  • hyperframes CLI 0.8.140
  • Skills from heygen-com/hyperframes; prefs-store.mjs is unchanged on main as of 2026-10-09
  • macOS 15 (Darwin 24.6), Node 24.14.1

Activity

  1. added
    bugSomething isn't working
    triage/readyAccepted scope, current evidence, acceptance criteria, and validation path are recorded.
    difficulty/easyLocalized change with a known approach, limited risk, and straightforward verification.
    on Oct 10, 2026
  2. miguel-heygen commented on Oct 24, 2026

    @miguel-heygen
    Collaborator

    Reproduced on current main (0.8.193) with your script: over 3 runs, two runs had 2 of the 4 calls fail with ENOENT on preferences.json.tmp, and one of those kept only 1 key, so a call that exited 0 still lost its value. Both causes are as you describe: the fixed temp name in writePrefsFile, and the unlocked read-modify-write in recordProjectTier. #5366 is open with a fix for both and is going through review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdifficulty/easyLocalized change with a known approach, limited risk, and straightforward verification.triage/readyAccepted scope, current evidence, acceptance criteria, and validation path are recorded.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions