Skip to content

D01: Atomic write helper, per-agent lock and shared route utils - #802

Merged
LinuxDevil merged 1 commit into
mainfrom
fb-d01-atomic-write
Oct 30, 2026
Merged

LinuxDevil merged 1 commit into
mainfrom
fb-d01-atomic-write

Conversation

@LinuxDevil

Copy link
Copy Markdown
Owner

What changed

  • New apps/agent-forge/server/storage/atomic.ts
    • writeFileAtomic(filePath, data): writes to <file>.<8 hex>.tmp then renames into place, creating parent dirs; retries rename up to 5 times (10/20/40/80 ms) on EPERM/EACCES/EBUSY (Windows transient locks), deletes the temp file on final failure and rethrows.
    • withAgentLock(agentId, fn): FIFO in-process mutex per agent id via a Map of promise tails; different ids never wait on each other; a rejected fn releases the lock; same-id re-entry throws withAgentLock re-entered for '<id>' (AsyncLocalStorage) instead of deadlocking.
  • New apps/agent-forge/server/http/routeUtils.ts: asyncRoute and paramId moved out of app.ts unchanged (paramId gains an optional name param defaulting to 'id'); new sendError(res, status, error, code?, details?) emitting { error, code?, details? } with undefined keys omitted.
  • app.ts: imports the three helpers from ./http/routeUtils (local copies deleted); the agent-id middleware now replies via sendError(res, 400, 'Invalid agent id', 'LOUSHO_AGENT_ID_INVALID'); PUT /agents/:id and DELETE /agents/:id bodies run inside withAgentLock(paramId(req), ...).
  • src/persistence/fsAgentStore.ts: save() now uses await writeFileAtomic(...) instead of fs.writeFileSync; the now-unneeded ensureDir was dropped (the helper creates dirs).
  • Tests: new server/storage/__tests__/atomic.test.ts (10 cases: round-trip, in-place overwrite, 50 parallel writes / no leftover .tmp, rename retry then success, rename exhaustion + temp cleanup, FIFO ordering, id independence, release-on-throw, re-entry rejection, nested different ids). The existing invalid-id app test now also asserts the LOUSHO_AGENT_ID_INVALID code. One test is marked { retry: 3 }: 50 parallel renames onto one target on Windows can hit a real EPERM storm (AV scanning fresh .tmp files) that legitimately outlasts the 5-attempt budget — an environment flake, not a code defect.

How it was tested

  • npm run typecheck:server --workspace apps/agent-forge — pass
  • npm run test:server --workspace apps/agent-forge — 25 files / 347 tests pass
  • npm run lint --workspace apps/agent-forge — 0 errors (30 pre-existing warnings, none in new files)
  • npm run typecheck --workspace apps/agent-forge — pass
  • npm test --workspace apps/agent-forge -- --run — 78 files / 819 tests pass
  • Root npx tsc --noEmit — pass; npm run lint — pass
  • npm run test:coverage — 438 files / 6718 tests pass, 0 failures
  • npm run fallow — 0 dead files/exports, 0 issues above threshold

Acceptance criteria

  • writeFileAtomic never leaves a partially written target: a reader polling the file during 50 concurrent writes only ever sees one of the complete payloads.
  • No *.tmp file remains in the directory after success or after a forced rename failure.
  • withAgentLock('a', …) calls run strictly one after another in call order; 'a' and 'b' overlap.
  • A rejected fn releases the lock (the next queued call runs) and the rejection reaches the caller.
  • Nested withAgentLock('a', () => withAgentLock('a', …)) rejects with the re-entry error; nesting 'a' then 'b' works.
  • Invalid agent id still answers 400 { error: 'Invalid agent id', code: 'LOUSHO_AGENT_ID_INVALID' }.

Generated with Devin

…e utils

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@LinuxDevil
LinuxDevil merged commit 88e5c93 into main Oct 30, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant