Repository navigation
D01: Atomic write helper, per-agent lock and shared route utils - #802
Merged
Merged
Conversation
…e utils Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
apps/agent-forge/server/storage/atomic.tswriteFileAtomic(filePath, data): writes to<file>.<8 hex>.tmpthen renames into place, creating parent dirs; retries rename up to 5 times (10/20/40/80 ms) onEPERM/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 aMapof promise tails; different ids never wait on each other; a rejectedfnreleases the lock; same-id re-entry throwswithAgentLock re-entered for '<id>'(AsyncLocalStorage) instead of deadlocking.apps/agent-forge/server/http/routeUtils.ts:asyncRouteandparamIdmoved out ofapp.tsunchanged (paramIdgains an optionalnameparam defaulting to'id'); newsendError(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 viasendError(res, 400, 'Invalid agent id', 'LOUSHO_AGENT_ID_INVALID');PUT /agents/:idandDELETE /agents/:idbodies run insidewithAgentLock(paramId(req), ...).src/persistence/fsAgentStore.ts:save()now usesawait writeFileAtomic(...)instead offs.writeFileSync; the now-unneededensureDirwas dropped (the helper creates dirs).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 theLOUSHO_AGENT_ID_INVALIDcode. One test is marked{ retry: 3 }: 50 parallel renames onto one target on Windows can hit a real EPERM storm (AV scanning fresh.tmpfiles) 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— passnpm run test:server --workspace apps/agent-forge— 25 files / 347 tests passnpm run lint --workspace apps/agent-forge— 0 errors (30 pre-existing warnings, none in new files)npm run typecheck --workspace apps/agent-forge— passnpm test --workspace apps/agent-forge -- --run— 78 files / 819 tests passnpx tsc --noEmit— pass;npm run lint— passnpm run test:coverage— 438 files / 6718 tests pass, 0 failuresnpm run fallow— 0 dead files/exports, 0 issues above thresholdAcceptance criteria
writeFileAtomicnever leaves a partially written target: a reader polling the file during 50 concurrent writes only ever sees one of the complete payloads.*.tmpfile 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.fnreleases the lock (the next queued call runs) and the rejection reaches the caller.withAgentLock('a', () => withAgentLock('a', …))rejects with the re-entry error; nesting'a'then'b'works.{ error: 'Invalid agent id', code: 'LOUSHO_AGENT_ID_INVALID' }.Generated with Devin