Fix race condition in cmdLock sort order

- Add deterministic tie-breaker (eventId) for lock events with identical timestamps in `src/lib.mjs`.
- Add regression tests in `test/lib.test.mjs` verifying win/loss conditions.
- Create weekly race condition report and run artifacts.
- Mark `race-condition-agent` task as completed.

Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
google-labs-jules[bot]andPR0M3TH3AN committed 2026-02-15 15:31:28 +00:00
1 parent fddd9ce494
commit 1d4097483c
12 files changed
+262 -1

No files matched your search

@@ -0,0 +1,18 @@
{
"cadence": "weekly",
"operation": "retrieve",
"runId": "session-mlnw868c",
"servicePath": "src/services/memory/index.js#getRelevantMemories",
"inputs": {
"agentId": "race-condition-agent",
"promptPath": "src/prompts/weekly/race-condition-agent.md",
"promptIntent": "## Required startup + artifacts + memory + issue capture",
"query": "scheduler | weekly | agent | race-condition-agent | intent | Required startup + artifacts + memory + issue capture",
"events": 1
},
"outputs": {
"ingestedCount": 1,
"retrievedCount": 1
},
"status": "ok"
}
@@ -0,0 +1 @@
MEMORY_RETRIEVED
@@ -0,0 +1,19 @@
{
"cadence": "weekly",
"operation": "store",
"runId": "session-mlnwbg1h",
"servicePath": "src/services/memory/index.js#ingestEvents",
"inputs": {
"agentId": "race-condition-agent",
"promptPath": "src/prompts/weekly/race-condition-agent.md",
"promptIntent": "## Required startup + artifacts + memory + issue capture",
"events": 2
},
"outputs": {
"storedCount": 1,
"summaries": [
"Store memory event A for weekly :: ## Required startup + artifacts + memory + issue capture Store memory event B for weekly :: ## Required startup + artifacts + memory + issue capture"
]
},
"status": "ok"
}
@@ -0,0 +1 @@
MEMORY_STORED
@@ -0,0 +1,23 @@
# Weekly Race Condition Report: 2026-02-15
## Focus Areas Audited
- Shared Client and Relay Pool (`src/lib.mjs`, `src/lock-ops.mjs`)
## Findings
### 1. Non-deterministic Lock Sorting in `cmdLock` (High Severity)
- **Description**: The `cmdLock` function in `src/lib.mjs` sorts contending lock events by `createdAt` timestamp only. Since Nostr `created_at` has 1-second resolution, two agents creating lock events in the same second could be sorted differently by different clients depending on the order in which relays return them (or local array sort stability if not handled).
- **Interleaving**:
1. Agent A creates lock event E1 at T=100.
2. Agent B creates lock event E2 at T=100.
3. Agent A queries relays, receives [E1, E2]. Sorts by time (equal). If stable sort or random order puts E1 first, A thinks it won.
4. Agent B queries relays, receives [E2, E1]. Sorts by time (equal). If stable sort or random order puts E2 first, B thinks it won.
5. Both agents proceed to execute the task, violating the mutual exclusion property.
- **Fix**: Implemented a deterministic tie-breaker using `eventId` (lexicographical comparison).
```javascript
.sort((a, b) => (a.createdAt - b.createdAt) || String(a.eventId).localeCompare(String(b.eventId)));
```
- **Risk**: Low (behavior preserving, standard distributed system practice).
## Next Week Focus
- App initialization and login flow.
@@ -0,0 +1,15 @@
---
agent: race-condition-agent
cadence: weekly
run-start: 2026-02-15T15:22:42Z
prompt: src/prompts/weekly/race-condition-agent.md
---
Goal: Audit for race conditions and fix one.
Scope:
- Shared Client and Relay Pool (`src/lib.mjs`, `src/lock-ops.mjs`)
- Lock acquisition logic
Constraints:
- Non-breaking changes
- One fix per run
@@ -0,0 +1,8 @@
---
agent: race-condition-agent
cadence: weekly
run-start: 2026-02-15T15:22:42Z
prompt: src/prompts/weekly/race-condition-agent.md
---
Decision: Use `eventId` (hex string) lexicographical sort as tie-breaker.
Rationale: Event IDs are unique and random (SHA256). Sorting by ID ensures that all clients agree on the order of events with identical timestamps, preventing split-brain scenarios where multiple agents think they won the lock.
+1 -1
View File
@@ -292,7 +292,7 @@ export async function cmdLock(agent, cadence, dryRun = false, deps = {}) {
const postLocks = await queryLocks(relays, cadence, dateStr, namespace);
const racingLocks = postLocks
.filter((l) => l.agent === agent)
.sort((a, b) => a.createdAt - b.createdAt);
.sort((a, b) => (a.createdAt - b.createdAt) || String(a.eventId).localeCompare(String(b.eventId)));
if (racingLocks.length > 1 && racingLocks[0].eventId !== event.id) {
const winner = racingLocks[0];
@@ -0,0 +1,64 @@
---
agent: race-condition-agent
cadence: weekly
run-start: 2026-02-15T15:22:42Z
prompt: src/prompts/weekly/race-condition-agent.md
---
Command: node --test test/lib.test.mjs
Result:
TAP version 13
# Subtest: src/lib.mjs
# Subtest: cmdCheck
# Subtest: returns correct structure with empty locks
ok 1 - returns correct structure with empty locks
# Subtest: identifies locked agents
ok 2 - identifies locked agents
# Subtest: excludes paused agents
ok 3 - excludes paused agents
# Subtest: identifies unknown locked agents
ok 4 - identifies unknown locked agents
1..4
ok 1 - cmdCheck
# Subtest: cmdLock
# Subtest: successfully locks an available agent
ok 1 - successfully locks an available agent
# Subtest: fails if agent is not in roster
ok 2 - fails if agent is not in roster
# Subtest: fails if agent is already locked
ok 3 - fails if agent is already locked
# Subtest: fails if agent is already completed
ok 4 - fails if agent is already completed
# Subtest: fails if race check is lost
ok 5 - fails if race check is lost
# Subtest: handles race condition with identical timestamps using eventId tie-breaker (we win)
ok 6 - handles race condition with identical timestamps using eventId tie-breaker (we win)
# Subtest: handles race condition with identical timestamps using eventId tie-breaker (we lose)
ok 7 - handles race condition with identical timestamps using eventId tie-breaker (we lose)
# Subtest: dry run does not publish
ok 8 - dry run does not publish
1..8
ok 2 - cmdLock
# Subtest: cmdList
# Subtest: lists active locks
ok 1 - lists active locks
# Subtest: warns about unknown agents
ok 2 - warns about unknown agents
# Subtest: handles no locks
ok 3 - handles no locks
1..3
ok 3 - cmdList
# Subtest: cmdComplete
# Subtest: successfully completes an active lock
ok 1 - successfully completes an active lock
# Subtest: fails if no active lock found
ok 2 - fails if no active lock found
# Subtest: detects already completed task
ok 3 - detects already completed task
1..3
ok 4 - cmdComplete
1..4
ok 1 - src/lib.mjs
1..1
# tests 18
# pass 18
# not-passed 0
+17
View File
@@ -0,0 +1,17 @@
---
agent: race-condition-agent
cadence: weekly
run-start: 2026-02-15T15:22:42Z
prompt: src/prompts/weekly/race-condition-agent.md
---
## Completed Tasks
- [x] Audit lock logic.
- [x] Implement fix in `src/lib.mjs`.
- [x] Add regression test in `test/lib.test.mjs`.
- [x] Verify tests pass.
- [x] Create weekly report.
- [x] Complete memory storage.
## Pending Tasks
- [ ] Verify artifacts.
- [ ] Complete lock.
@@ -0,0 +1,7 @@
---
agent: race-condition-agent
cadence: weekly
status: completed
platform: linux
---
Fixed race condition in lock acquisition logic (`src/lib.mjs`) by adding `eventId` tie-breaker. Verified with regression tests.
+88
View File
@@ -199,6 +199,94 @@ describe('src/lib.mjs', () => {
assert.match(logs.join('\n'), /LOCK_STATUS=race_lost/);
});
it('handles race condition with identical timestamps using eventId tie-breaker (we win)', async () => {
let callCount = 0;
const deps = {
...baseDeps,
queryLocks: async () => {
callCount++;
if (callCount === 1) return [];
return [{
agent: 'agent1',
eventId: 'aaaaa', // competitor (higher ID)
createdAt: 100,
createdAtIso: 'time'
}, {
agent: 'agent1',
eventId: 'mock-id', // our ID (we need to ensure mockFinalizeEvent returns this or we control comparison)
// The test mockFinalizeEvent returns 'mock-id'.
// 'mock-id' > 'aaaaa'. So we would lose if competitor is 'aaaaa'.
// To win, competitor must be 'zzzzz'.
// Let's re-check the logic.
// sort((a, b) => ... || a.id.localeCompare(b.id))
// If we are 'mock-id', and competitor is 'zzzzz'.
// 'mock-id'.localeCompare('zzzzz') is negative. So we come first.
// racingLocks[0] is us. We win.
createdAt: 100,
}];
},
publishLock: async () => {},
generateSecretKey: mockGenerateSecretKey,
getPublicKey: mockGetPublicKey,
finalizeEvent: (t) => ({ ...t, id: 'mock-id' }),
raceCheckDelayMs: 1,
};
// Competitor is 'aaaaa'. We are 'mock-id'. 'aaaaa' comes first. We lose.
// Wait, I want a test where we WIN.
// So competitor should be 'zzzzz'.
deps.queryLocks = async () => {
callCount++;
if (callCount === 1) return [];
return [{
agent: 'agent1',
eventId: 'zzzzz', // competitor (higher ID)
createdAt: 100,
createdAtIso: 'time'
}, {
agent: 'agent1',
eventId: 'mock-id', // our ID
createdAt: 100,
}];
};
const result = await cmdLock('agent1', 'daily', false, deps);
assert.strictEqual(result.status, 'ok');
assert.match(logs.join('\n'), /LOCK_STATUS=ok/);
});
it('handles race condition with identical timestamps using eventId tie-breaker (we lose)', async () => {
let callCount = 0;
const deps = {
...baseDeps,
queryLocks: async () => {
callCount++;
if (callCount === 1) return [];
return [{
agent: 'agent1',
eventId: 'aaaaa', // competitor (lower ID)
createdAt: 100,
createdAtIso: 'time'
}, {
agent: 'agent1',
eventId: 'mock-id', // our ID ('m' > 'a')
createdAt: 100,
}];
},
publishLock: async () => {},
generateSecretKey: mockGenerateSecretKey,
getPublicKey: mockGetPublicKey,
finalizeEvent: (t) => ({ ...t, id: 'mock-id' }),
raceCheckDelayMs: 1,
};
await assert.rejects(
async () => await cmdLock('agent1', 'daily', false, deps),
(err) => err.code === 3 && err.message === 'Race check lost'
);
assert.match(logs.join('\n'), /LOCK_STATUS=race_lost/);
});
it('dry run does not publish', async () => {
let published = false;
const deps = {