diff --git a/.beads/traces/bf-3jyx/metadata.json b/.beads/traces/bf-3jyx/metadata.json new file mode 100644 index 0000000..3f91674 --- /dev/null +++ b/.beads/traces/bf-3jyx/metadata.json @@ -0,0 +1,16 @@ +{ + "bead_id": "bf-3jyx", + "agent": "claude-sonnet", + "provider": "anthropic", + "model": "claude-sonnet-4-6", + "exit_code": 1, + "outcome": "failure", + "duration_ms": 179856, + "input_tokens": null, + "output_tokens": null, + "cost_usd": null, + "captured_at": "2026-05-15T20:54:16.080059559Z", + "trace_format": "claude_json", + "pruned": false, + "template_version": null +} \ No newline at end of file diff --git a/.beads/traces/bf-3jyx/stderr.txt b/.beads/traces/bf-3jyx/stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/.beads/traces/bf-3jyx/stdout.txt b/.beads/traces/bf-3jyx/stdout.txt new file mode 100644 index 0000000..9fc7a6a --- /dev/null +++ b/.beads/traces/bf-3jyx/stdout.txt @@ -0,0 +1 @@ +Error: Reached max turns (30) diff --git a/src/store.test.ts b/src/store.test.ts index 4c60d2a..8f6b5b3 100644 --- a/src/store.test.ts +++ b/src/store.test.ts @@ -200,6 +200,37 @@ describe('InMemoryEventStore', () => { expect(worker?.beadsCompleted).toBe(2); }); + it('should set status to idle on bead.released with release_success', () => { + store.add(createEvent({ worker: 'w-test', msg: 'bead.released', reason: 'release_success', bead: 'bd-1' })); + + const worker = store.getWorker('w-test'); + expect(worker?.status).toBe('idle'); + }); + + it('should increment beadsCompleted on bead.released with release_success', () => { + store.add(createEvent({ worker: 'w-test', msg: 'bead.released', reason: 'release_success', bead: 'bd-1' })); + store.add(createEvent({ worker: 'w-test', msg: 'bead.released', reason: 'release_success', bead: 'bd-2' })); + + const worker = store.getWorker('w-test'); + expect(worker?.beadsCompleted).toBe(2); + }); + + it('should clear activeBead and activeFiles on bead.released with release_success', () => { + store.add(createEvent({ worker: 'w-test', msg: 'bead.claimed', bead: 'bd-1' })); + store.add(createEvent({ worker: 'w-test', msg: 'bead.released', reason: 'release_success', bead: 'bd-1' })); + + const worker = store.getWorker('w-test'); + expect(worker?.activeBead).toBeUndefined(); + expect(worker?.activeFiles).toEqual([]); + }); + + it('should NOT increment beadsCompleted on bead.released without release_success', () => { + store.add(createEvent({ worker: 'w-test', msg: 'bead.released', reason: 'release_retry', bead: 'bd-1' })); + + const worker = store.getWorker('w-test'); + expect(worker?.beadsCompleted).toBe(0); + }); + it('should track firstSeen timestamp', () => { const earlyTs = 1000; const lateTs = 5000; diff --git a/src/store.ts b/src/store.ts index b5510e3..1fbd693 100644 --- a/src/store.ts +++ b/src/store.ts @@ -484,7 +484,8 @@ export class InMemoryEventStore implements EventStore { // Find the completion event for this bead const completionEvent = this.events.find(e => e.bead === beadId && - (e.msg === 'bead.completed' || e.msg === 'bead.failed') + (e.msg === 'bead.completed' || e.msg === 'bead.failed' || + (e.msg === 'bead.released' && e['reason'] === 'release_success')) ); if (completionEvent) { @@ -686,6 +687,14 @@ export class InMemoryEventStore implements EventStore { worker.activeDirectories = []; worker.activeBead = undefined; } + } else if (needleEvent === 'bead.released' && event['reason'] === 'release_success') { + worker.status = 'idle'; + if (event.bead) { + worker.beadsCompleted++; + } + worker.activeFiles = []; + worker.activeDirectories = []; + worker.activeBead = undefined; } else if ( needleEvent === 'worker.started' || needleEvent === 'bead.claimed' || @@ -746,7 +755,8 @@ export class InMemoryEventStore implements EventStore { // Check for task completion — match on NEEDLE event type exactly const msg = event.msg || ''; - if (msg === 'bead.completed' || msg === 'bead.failed') { + const isReleasedSuccess = msg === 'bead.released' && event['reason'] === 'release_success'; + if (msg === 'bead.completed' || msg === 'bead.failed' || isReleasedSuccess) { const startTime = this.taskStartTimes.get(beadId); if (startTime) { const durationMs = event.ts - startTime; diff --git a/src/workerAnalytics.test.ts b/src/workerAnalytics.test.ts index b3b72d3..0f1dabf 100644 --- a/src/workerAnalytics.test.ts +++ b/src/workerAnalytics.test.ts @@ -144,6 +144,32 @@ describe('WorkerAnalytics', () => { }); }); + describe('bead.released tracking', () => { + it('should count bead.released with release_success as a completion', () => { + const events: LogEvent[] = [ + { ts: baseTime, worker: 'w-1', level: 'info', msg: 'bead.released', reason: 'release_success', bead: 'bd-1' }, + { ts: baseTime + 1000, worker: 'w-1', level: 'info', msg: 'bead.released', reason: 'release_success', bead: 'bd-2' }, + ]; + + events.forEach(e => analytics.processEvent(e)); + + const metrics = analytics.getWorkerMetrics('w-1'); + expect(metrics?.beadsCompleted).toBe(2); + }); + + it('should NOT count bead.released with other reasons as a completion', () => { + const events: LogEvent[] = [ + { ts: baseTime, worker: 'w-1', level: 'info', msg: 'bead.released', reason: 'release_retry', bead: 'bd-1' }, + { ts: baseTime + 1000, worker: 'w-1', level: 'info', msg: 'bead.released', bead: 'bd-2' }, + ]; + + events.forEach(e => analytics.processEvent(e)); + + const metrics = analytics.getWorkerMetrics('w-1'); + expect(metrics?.beadsCompleted).toBe(0); + }); + }); + describe('Error Tracking', () => { it('should track error count', () => { const events: LogEvent[] = [ diff --git a/src/workerAnalytics.ts b/src/workerAnalytics.ts index 4916203..952f09f 100644 --- a/src/workerAnalytics.ts +++ b/src/workerAnalytics.ts @@ -695,11 +695,12 @@ export class WorkerAnalytics implements WorkerAnalyticsStore { // Detect bead completion const msg = event.msg?.toLowerCase() || ''; + const isReleasedSuccess = event.msg === 'bead.released' && event['reason'] === 'release_success'; if ( msg.includes('completed') || msg.includes('finished') || msg.includes('done') || - msg.includes('success') + isReleasedSuccess ) { const startTime = worker.beadStartTimes.get(beadId); if (startTime) {