Skip to content

fix(governance): move TMP_PATH write inside lock in GoalManager.save() (PKT-563) - #454

Open
bugsyhewitt wants to merge 1 commit into
mainfrom
fix/563-goal-manager-save-concurrent-write-race
Open

fix(governance): move TMP_PATH write inside lock in GoalManager.save() (PKT-563)#454
bugsyhewitt wants to merge 1 commit into
mainfrom
fix/563-goal-manager-save-concurrent-write-race

Conversation

@bugsyhewitt

Copy link
Copy Markdown
Owner

Summary

  • GoalManager.save() wrote to shared TMP_PATH before acquiring the lock. Two concurrent callers both ran writeFileSync(TMP_PATH, ...) before either held the lock, so whichever caller won the lock renamed a TMP file containing the other caller's data — silent data inversion. The losing caller then hit ENOENT on its rename (TMP was gone).
  • Fix: move writeFileSync(TMP_PATH, ...) inside the acquireLock/releaseLock critical section (2 LOC moved). Write + rename now execute atomically under the lock with no yield points between them.
  • Adds three concurrent-save tests: both saves fulfill (no ENOENT), on-disk self-consistency, no TMP orphan.

TDD evidence

Failing-first (buggy code):

× GoalManager.save() — concurrent writes (PKT-563 race fix) > both concurrent saves fulfill — no ENOENT from shared TMP_PATH race
AssertionError: expected 'rejected' to be 'fulfilled'
Tests 1 failed | 43 passed (44)

After fix: Tests 44 passed (44)

Ship-gate

pnpm test (vitest 2186 passed + 46 skipped, pytest 1241 passed + 125 skipped) — exit 0.

Files changed

  • src/alienclaw/governance/common/goal-manager.ts — 2-LOC move + comment corrected from tmp → lock to lock → tmp → rename → release
  • test/governance/common/goal-manager.test.ts — section 15 (3 new concurrent-save tests)

Related

  • PKT-079 (goal-manager unit tests — single-caller coverage only, no concurrent)
  • PKT-559 (different method: getReadySubGoals/getReadyCampaigns undefined dependsOn)

🤖 Generated with Claude Code

…) (PKT-563)

Concurrent save() calls shared TMP_PATH before acquireLock(), allowing a
second caller to overwrite the first's tmp data before the first renames
it — producing silent data inversion and ENOENT on the losing caller.

Move writeFileSync(TMP_PATH, ...) inside the acquireLock/releaseLock
critical section so each save() owns TMP_PATH for the full
write→rename window.

Adds three concurrent-save tests covering: both saves fulfill, on-disk
self-consistency, and no TMP_PATH orphan.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@bugsyhewitt bugsyhewitt added the auto-merge-ok Binding review passed + ship-gate green — cleared for auto-merge label Aug 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto-merge-ok Binding review passed + ship-gate green — cleared for auto-merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant