mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
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:
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
@@ -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
|
||||
@@ -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.
|
||||
@@ -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 = {
|
||||
|
||||
Reference in new issue
Block a user