Repository navigation
Description
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.jsonExpected 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
- Use a unique temporary name, as
scripts/lib/telemetry.mjsalready does:${path}.${process.pid}.tmp.manifest.mjsuses random bytes for the same purpose. This removes theENOENT. - To also stop lost updates, serialise the read-modify-write in
recordPreference(). For example, take a lock withmkdirSync(${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.mjsis unchanged onmainas of 2026-10-09 - macOS 15 (Darwin 24.6), Node 24.14.1
Activity
miguel-heygen commentedon Oct 24, 2026
Reproduced on current main (0.8.193) with your script: over 3 runs, two runs had 2 of the 4 calls fail with
ENOENTonpreferences.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 inwritePrefsFile, and the unlocked read-modify-write inrecordProjectTier. #5366 is open with a fix for both and is going through review.