feat(bf-3jyx): fire beadsCompleted on bead.released/release_success
Modern NEEDLE emits bead.released with reason=release_success instead of
legacy bead.completed. Update store.ts and workerAnalytics.ts to handle
this event: increment beadsCompleted, set status to idle, and clear
activeBead/activeFiles/activeDirectories.
Also fixes workerAnalytics trackBeadEvent to use explicit isReleasedSuccess
check instead of broad msg.includes('success') which could false-positive.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
cffeb26e79
commit
c36ce6da37
7 changed files with 88 additions and 3 deletions
16
.beads/traces/bf-3jyx/metadata.json
Normal file
16
.beads/traces/bf-3jyx/metadata.json
Normal file
|
|
@ -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
|
||||
}
|
||||
0
.beads/traces/bf-3jyx/stderr.txt
Normal file
0
.beads/traces/bf-3jyx/stderr.txt
Normal file
1
.beads/traces/bf-3jyx/stdout.txt
Normal file
1
.beads/traces/bf-3jyx/stdout.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
Error: Reached max turns (30)
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
14
src/store.ts
14
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;
|
||||
|
|
|
|||
|
|
@ -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[] = [
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue