fix(bf-27e4): unify stuck detection metric with beadsCompleted
Some checks are pending
CI / test (18.x) (push) Waiting to run
CI / test (20.x) (push) Waiting to run
CI / test (22.x) (push) Waiting to run

Fix discrepancy where /api/workers returned contradictory data:
- beadsCompleted: 285 (counts bead.released events including timed-out)
- stuck: true, stuckReason: 'Running for 2311m with only 1 completion(s)'

The stuck detection now correctly uses:
- beadsCompleted: all beads processed (including timed-out/deferred)
- beadsSucceeded: only successful completions (bead.completed events)
- beadsTimedOut: new counter for timed-out/deferred beads

Changes:
- Add beadsTimedOut counter to WorkerInfo type
- Increment beadsTimedOut on bead.released with TimedOut/Deferred outcome
- Update stuck detection to show clear reason text:
  - 'X processed but 0 successful completions (all timed out/deferred)'
  - 'X processed but only Y successful completion(s) (Z timed out/deferred)'
- Add beadsTimedOut to evidence array

Fix acceptance criteria:
- Worker processing 100 timed-out beads shows clearly in UI:
  - 100 beads completed
  - 0 beads succeeded
  - Stuck reason: '100 processed but 0 successful completions'

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-06-07 11:11:50 -04:00
parent c627791356
commit 47c3396e0c
17 changed files with 5885 additions and 12957 deletions

View file

@ -143,7 +143,7 @@
{"id":"bf-1373","title":"Fix: DiffView.parseDiff broken (added/removed line parsing fails)","description":"3 tests in src/tui/components/DiffView.test.ts fail in the parseDiff suite:\n- \"should parse added lines\"\n- \"should parse removed lines\"\n- \"should handle empty diff\"\n\nThe parseDiff function (exported from DiffView.ts) is not correctly identifying + and - lines in unified diff output, or the parsing of the old_string/new_string diff computation is incorrect.\n\nFix: audit parseDiff() in src/tui/components/DiffView.ts, ensure it correctly:\n- Splits diff on newlines\n- Classifies lines starting with '+' as added (excluding '+++')\n- Classifies lines starting with '-' as removed (excluding '---')\n- Returns empty array for empty/null input","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":1,"issue_type":"task","created_at":"2026-05-02T18:18:17.589533196Z","updated_at":"2026-05-02T20:04:01.643730649Z","closed_at":"2026-05-02T20:04:01.643730649Z","close_reason":"Completed","source_repo":".","compaction_level":0}
{"id":"bf-1nah","title":"Fix bin/fabric entrypoint and install systemd service","description":"Two infrastructure gaps preventing fabric from running:\n\n1. bin/fabric is an empty file (0 bytes). The package.json declares bin: {'fabric': './dist/cli.js'}, so npm install -g should create a 'fabric' executable — but the bin/ directory entry is empty/unused (dist/cli.js IS the real entrypoint). Fix: either remove the empty bin/fabric file and rely solely on the package.json bin declaration, or make bin/fabric a shebang wrapper:\n #!/usr/bin/env node\n require('../dist/cli.js');\n Either way, verify that 'node dist/cli.js --help' works and that the bin entry actually resolves.\n\n2. The systemd user service has never been installed. The unit file exists at scripts/fabric-web.service. Install it:\n mkdir -p ~/.config/systemd/user\n cp scripts/fabric-web.service ~/.config/systemd/user/\n Also install the prune timer:\n cp scripts/fabric-prune.service ~/.config/systemd/user/\n cp scripts/fabric-prune.timer ~/.config/systemd/user/\n systemctl --user daemon-reload\n systemctl --user enable fabric-web.service\n systemctl --user start fabric-web.service\n Verify: systemctl --user status fabric-web.service shows 'active (running)' and curl -s http://localhost:3000/api/workers returns JSON.\n Also verify the secrets file exists at ~/.config/fabric/secrets.env with FABRIC_AUTH_TOKEN set (create with a random token if missing).\n\nAcceptance: systemctl --user status fabric-web.service is active, curl http://localhost:3000/api/workers returns valid JSON.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":1,"issue_type":"task","assignee":"marathon","created_at":"2026-05-26T21:00:11.965674425Z","updated_at":"2026-05-26T21:05:46.348779251Z","closed_at":"2026-05-26T21:05:46.348779251Z","close_reason":"Commit 67f991a: Fixed systemd service node paths for NixOS (/usr/bin/node -> /home/coding/.nix-profile/bin/node), removed empty bin/fabric file, created ~/.config/fabric/secrets.env, installed and enabled fabric-web.service and fabric-prune.timer. Acceptance verified: systemctl --user status fabric-web.service active (running), curl http://localhost:3000/api/workers returns valid JSON.","source_repo":".","compaction_level":0}
{"id":"bf-1uu9","title":"E2E OTLP integration test: POST mock NEEDLE spans to :4318 and verify /api/workers updates","description":"## Goal\nAdd a vitest integration test that:\n1. Starts the FABRIC web server with --otlp-http on a random port\n2. POSTs a realistic NEEDLE OTLP payload (spans + metrics with needle.worker.id, needle.bead.id, needle.session.id attributes) to /v1/traces and /v1/metrics\n3. Asserts GET /api/workers returns a worker entry with the correct worker ID and a non-STOPPED needleState\n4. Asserts GET /api/summary returns workers_active >= 1\n\n## Why\nThe OTLP receiver (otlpHttpReceiver.ts + normalizer.ts) is implemented but has no integration-level test covering the full HTTP → normalizer → store → API response path.\n\n## Acceptance criteria\n- Test lives in src/ or tests/ (vitest compatible)\n- Uses real HTTP — start server, POST protobuf or JSON OTLP, check API\n- npm test passes","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":1,"issue_type":"task","assignee":"claude-code-glm-4.7-charlie","created_at":"2026-05-30T13:52:38.619910418Z","updated_at":"2026-06-07T13:50:35.990123171Z","closed_at":"2026-06-07T13:50:35.990123171Z","close_reason":"Completed: Added active workers count test case to existing OTLP E2E integration test. The test file already existed with 6 passing tests; added 1 more to count non-STOPPED workers. All tests pass. Note: /api/summary endpoint does not exist; used /api/workers instead.","source_repo":".","compaction_level":0}
{"id":"bf-27e4","title":"Fix beadsCompleted vs stuck detection metric discrepancy in /api/workers response","description":"## Problem\n/api/workers returns contradictory data per worker:\n- beadsCompleted: 285 (counts bead.released events)\n- stuck: true, stuckReason: 'Running for 2311m with only 1 completion(s)'\n\nThe stuck detection counts a different metric (outcome.success events or similar) while beadsCompleted counts bead.released. When all beads time out and are deferred, beadsCompleted increments but the stuck detector sees zero success outcomes and flags the worker as stuck.\n\n## Fix options\n1. Unify the metric — stuck detection should use the same counter as beadsCompleted\n2. Or: update stuckReason to clarify it means 'zero successful completions' not 'zero total processed'\n3. Or: add a separate 'beadsTimedOut' counter and show it in the worker card\n\n## Acceptance criteria\n- A worker that processes 100 beads (all timed out) shows clearly in the UI that it processed 100 but completed 0 successfully — not a confusing mix of 100 and 'only 1 completion'\n- The stuck flag should still fire but the reason text should be accurate","design":"","acceptance_criteria":"","notes":"","status":"in_progress","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-charlie","created_at":"2026-05-30T13:52:47.322897128Z","updated_at":"2026-06-07T14:53:16.952307554Z","source_repo":".","compaction_level":0}
{"id":"bf-27e4","title":"Fix beadsCompleted vs stuck detection metric discrepancy in /api/workers response","description":"## Problem\n/api/workers returns contradictory data per worker:\n- beadsCompleted: 285 (counts bead.released events)\n- stuck: true, stuckReason: 'Running for 2311m with only 1 completion(s)'\n\nThe stuck detection counts a different metric (outcome.success events or similar) while beadsCompleted counts bead.released. When all beads time out and are deferred, beadsCompleted increments but the stuck detector sees zero success outcomes and flags the worker as stuck.\n\n## Fix options\n1. Unify the metric — stuck detection should use the same counter as beadsCompleted\n2. Or: update stuckReason to clarify it means 'zero successful completions' not 'zero total processed'\n3. Or: add a separate 'beadsTimedOut' counter and show it in the worker card\n\n## Acceptance criteria\n- A worker that processes 100 beads (all timed out) shows clearly in the UI that it processed 100 but completed 0 successfully — not a confusing mix of 100 and 'only 1 completion'\n- The stuck flag should still fire but the reason text should be accurate","design":"","acceptance_criteria":"","notes":"","status":"in_progress","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-charlie","created_at":"2026-05-30T13:52:47.322897128Z","updated_at":"2026-06-07T15:05:28.387939688Z","source_repo":".","compaction_level":0}
{"id":"bf-2q9r","title":"Add per-needle-worker MemoryMax ceiling (4 GB) so no single worker can exhaust the cgroup","description":"Problem: with only a cgroup-level soft limit, one runaway worker can still consume all available memory before pressure kills it.\n\nSolution: apply a per-process MemoryMax to each needle worker. Options:\n1. systemd transient scope: needle spawns workers with systemd-run --scope -p MemoryMax=4G\n2. needle config: check if needle supports resource limits in its worker launch config\n3. cgroup v2 direct: write 4G to memory.max for each worker cgroup after spawn\n\nTarget: each Claude Code session bounded at 4 GB RSS. With 6 workers + fabric-web + VSCode that stays well under 32 GB.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":0,"issue_type":"task","assignee":"claude-code-glm-4.7-charlie","created_at":"2026-05-27T11:11:08.152673301Z","updated_at":"2026-06-07T13:24:49.273630596Z","closed_at":"2026-06-07T13:24:49.273630596Z","close_reason":"Completed","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"compacted_at_commit":"","sender":"","comments":[{"id":15,"issue_id":"bf-2q9r","author":"cli","text":"## Summary\n\nImplemented per-needle-worker MemoryMax ceiling (4 GB) using cgroup v2 direct approach (writing to memory.max). Each needle worker is now bounded at 4 GB RSS, preventing runaway workers from exhausting the cgroup's memory.\n\n## Implementation\n\n- Created src/workerMemoryLimiter.ts with core logic for:\n - Finding worker PIDs by reading /proc cmdline and matching needle run --identifier <value>\n - Getting cgroup v2 paths from /proc/<pid>/cgroup\n - Applying memory limits by writing to memory.max\n - Cache of limited workers to avoid redundant work\n - Helper functions for reading memory usage/limits\n\n- Integrated into src/cli.ts:\n - Apply limits at startup for both tui and web commands (directory source only)\n - Log count of limited workers to stderr\n\n- Integrated into src/directoryTailer.ts:\n - Apply limits when new log files are detected\n - Auto-limits workers as they come online\n\n## Retrospective\n\n- **What worked:** The cgroup v2 direct approach is simple and effective. Writing to memory.max requires no systemd integration and works immediately. The process discovery via /proc/<pid>/cmdline reliably finds needle workers by matching the --identifier argument.\n\n- **What didn't:** Initial approach considered systemd transient scope (systemd-run --scope -p MemoryMax=4G) but would require needle to spawn workers differently. Cgroup v2 direct approach works without changing needle's launch process.\n\n- **Surprise:** The random 8-char hex suffix in log filenames (e.g., claude-code-glm-4.7-alpha-2wf3a1b2.jsonl) required careful parsing to extract the worker identifier for PID matching.\n\n- **Reusable pattern:** Per-process resource limiting via cgroup v2 is a general pattern. Reading /proc/<pid>/cgroup to get the path and writing to the appropriate controller file works for memory, cpu, io, etc.","created_at":"2026-06-07T13:22:21.566362525Z"}],"annotations":{"completion":"Summary of work completed: Added per-needle-worker MemoryMax ceiling (4 GB) to prevent single worker from exhausting the cgroup.\n\n## Implementation\n- Created `workerMemoryLimiter.ts` implementing cgroup v2 direct approach (writing to memory.max)\n- Default limit: 4 GB per worker (configurable)\n- Integration at two points:\n 1. CLI startup: `applyAllWorkerLimits()` called in both tui and web commands\n 2. DirectoryTailer: `applyLimitForLogFile()` called when new log files are activated\n- Worker discovery via log file naming pattern and PID lookup in /proc\n\n## Retrospective\n- **What worked:** Cgroup v2 direct approach is clean and requires no external dependencies\n- **What didn't:** systemd-run --scope would require needle changes\n- **Surprise:** PID discovery via /proc/cmdline is more reliable than log file parsing alone\n- **Reusable pattern:** For cgroup v2 resource limits: read /proc/<pid>/cgroup, then write to /sys/fs/cgroup/<path>/memory.max"}}
{"id":"bf-2wf","title":"Phase 9: Productivity Analytics — remaining gaps","description":"Tracks unfinished Phase 9 items from docs/plan.md (Productivity Analytics). DONE already (verified in code 2026-05-22): beadsCompleted fires on bead.released/release_success (store.ts), worker sort by state, test-worker filter (isTestWorker + hideTestWorkers toggle), Productivity panel daily-throughput chart + worker leaderboard, GET /api/productivity. REMAINING (this epic): currentBead field, fleet summary bar, worker-card enrichment, bead workspace scanner + project breakdown. See docs/plan.md section Phase 9.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":1,"issue_type":"epic","assignee":"claude-code-glm-4.7-echo","created_at":"2026-05-22T19:19:43.513243795Z","updated_at":"2026-05-22T22:02:57.997168418Z","closed_at":"2026-05-22T22:02:57.997168418Z","close_reason":"Phase 9 Productivity Analytics verification complete. All items were already implemented in prior sessions. See notes/bf-2wf.md for details.","source_repo":".","compaction_level":0,"labels":["phase9"],"dependencies":[{"issue_id":"bf-2wf","depends_on_id":"bf-60j","type":"blocks","created_at":"2026-05-22T19:21:15.280109842Z","created_by":"cli","thread_id":""},{"issue_id":"bf-2wf","depends_on_id":"bf-3xp","type":"blocks","created_at":"2026-05-22T19:21:15.283895541Z","created_by":"cli","thread_id":""},{"issue_id":"bf-2wf","depends_on_id":"bf-4f3","type":"blocks","created_at":"2026-05-22T19:21:15.287460586Z","created_by":"cli","thread_id":""},{"issue_id":"bf-2wf","depends_on_id":"bf-3t8","type":"blocks","created_at":"2026-05-22T19:21:15.291058758Z","created_by":"cli","thread_id":""}]}
{"id":"bf-30p4","title":"Fix: FileContextPanel 29 test failures (constructor, bindings, render, show/hide)","description":"29 of 57 tests in src/tui/components/FileContextPanel.test.ts fail, covering:\n- constructor: key handlers not bound on construction\n- setContextFromEvent: scroll offset not reset on new context\n- setContent: render not triggered when updating current file\n- syntax highlighting: TS/JS/Python/Rust/unknown file type detection\n- operation icons: read/edit/write/glob icon selection\n- recent files navigation: prev/next file navigation\n- show/hide/toggle: panel visibility methods\n- focus: focus() not delegating to box element\n- getElement: getElement() method missing or returning wrong element\n- clear: render not triggered after clear\n- key bindings: scroll up/down/page keys, open-in-editor key not bound\n- render output: no-file message, file path in header, directory path, operation history\n- regression: operation type detection\n\nThis is a broad failure suggesting FileContextPanel was written to a different API than what the tests expect, or the constructor wiring is broken.\n\nFix: audit FileContextPanel constructor and public API against the test expectations; ensure all key bindings, render, show/hide, focus methods are correctly implemented.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":1,"issue_type":"task","assignee":"claude-code-glm-4.7-juliet","created_at":"2026-05-02T18:18:36.164020387Z","updated_at":"2026-05-22T20:32:56.398383689Z","closed_at":"2026-05-22T20:32:56.398383689Z","close_reason":"Completed - all 57 tests passing","source_repo":".","compaction_level":0}

View file

@ -5,11 +5,11 @@
"model": "glm-4.7",
"exit_code": 1,
"outcome": "failure",
"duration_ms": 220625,
"duration_ms": 248738,
"input_tokens": null,
"output_tokens": null,
"cost_usd": null,
"captured_at": "2026-06-07T14:56:58.154738375Z",
"captured_at": "2026-06-07T15:09:37.379920954Z",
"trace_format": "claude_json",
"pruned": false,
"template_version": null

View file

@ -1,3 +1,3 @@
Running as unit: run-p1010356-i9397831.scope; invocation ID: 183ffff0f11b44e1a3b2639417e5b01e
Running as unit: run-p1023704-i9411179.scope; invocation ID: 54bb86cafab54563b37739e2f28b867b
SessionEnd hook [/home/coding/.ccdash/hooks/session-end.sh] failed: /bin/sh: line 1: /home/coding/.ccdash/hooks/session-end.sh: cannot execute: required file not found

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
04739044342ad0375ba0a9299677ff8caceea590
c6277913561e0379270b67e672b66ba8316bde4c

View file

@ -623,6 +623,7 @@ export class InMemoryEventStore implements EventStore {
status: 'active',
beadsCompleted: 0,
beadsSucceeded: 0,
beadsTimedOut: 0,
firstSeen: event.ts,
lastActivity: event.ts,
activeFiles: [],
@ -713,6 +714,11 @@ export class InMemoryEventStore implements EventStore {
// bead.released with release_success includes timed-out/deferred beads
if (event.bead) {
worker.beadsCompleted++;
// Track timed-out/deferred beads separately
const releaseOutcome = event['outcome'] as string | undefined;
if (releaseOutcome === 'TimedOut' || releaseOutcome === 'Deferred') {
worker.beadsTimedOut++;
}
}
worker.activeFiles = [];
worker.activeDirectories = [];

View file

@ -193,6 +193,8 @@ function createMockWorker(overrides: Partial<WorkerInfo> = {}): WorkerInfo {
id: 'w-test123',
status: 'active',
beadsCompleted: 5,
beadsSucceeded: 3,
beadsTimedOut: 2,
firstSeen: Date.now() - 60000,
lastActivity: Date.now(),
activeFiles: [],

View file

@ -42,6 +42,8 @@ function createMockWorker(overrides: Partial<WorkerInfo> = {}): WorkerInfo {
id: 'w-test123',
status: 'active',
beadsCompleted: 5,
beadsSucceeded: 3,
beadsTimedOut: 2,
firstSeen: Date.now() - 60000,
lastActivity: Date.now(),
activeFiles: [],
@ -176,7 +178,11 @@ describe('E2E: WorkerDetail Panel', () => {
describe('beads completed display', () => {
it('should display beads completed count with green color', () => {
detail.setWorker(createMockWorker({ beadsCompleted: 42 }));
detail.setWorker(createMockWorker({
beadsCompleted: 42,
beadsSucceeded: 3,
beadsTimedOut: 2,
}));
const content = getRenderedContent();
expect(content).toContain('Beads Completed:');
@ -184,7 +190,11 @@ describe('E2E: WorkerDetail Panel', () => {
});
it('should display zero beads completed', () => {
detail.setWorker(createMockWorker({ beadsCompleted: 0 }));
detail.setWorker(createMockWorker({
beadsCompleted: 0,
beadsSucceeded: 0,
beadsTimedOut: 0,
}));
const content = getRenderedContent();
expect(content).toContain('{green-fg}0{/}');
@ -394,6 +404,8 @@ describe('E2E: WorkerDetail Panel', () => {
id: 'w-claude-sonnet-alpha',
status: 'active',
beadsCompleted: 23,
beadsSucceeded: 3,
beadsTimedOut: 2,
firstSeen: Date.now() - 5400000, // ~1.5h ago
lastEvent: {
ts: Date.now() - 5000,

View file

@ -70,6 +70,8 @@ function createMockWorker(overrides: Partial<WorkerInfo> = {}): WorkerInfo {
id: 'w-test123',
status: 'active',
beadsCompleted: 5,
beadsSucceeded: 3,
beadsTimedOut: 2,
firstSeen: Date.now() - 60000,
lastActivity: Date.now(),
activeFiles: ['/src/test.ts'],
@ -277,7 +279,11 @@ describe('WorkerDetail', () => {
});
it('should include beads completed count', () => {
const worker = createMockWorker({ beadsCompleted: 42 });
const worker = createMockWorker({
beadsCompleted: 42,
beadsSucceeded: 3,
beadsTimedOut: 2,
});
workerDetail.setWorker(worker);
const content = mockBoxInstance.setContent.mock.calls[0][0];

View file

@ -40,6 +40,8 @@ function createMockWorker(overrides: Partial<WorkerInfo> = {}): WorkerInfo {
id: 'w-test123',
status: 'active',
beadsCompleted: 5,
beadsSucceeded: 3,
beadsTimedOut: 2,
firstSeen: Date.now() - 60000,
lastActivity: Date.now(),
activeFiles: [],

View file

@ -39,6 +39,8 @@ function createMockWorker(overrides: Partial<WorkerInfo> = {}): WorkerInfo {
id: 'w-test123',
status: 'active',
beadsCompleted: 5,
beadsSucceeded: 3,
beadsTimedOut: 2,
firstSeen: Date.now() - 60000,
lastActivity: Date.now(),
activeFiles: [],

View file

@ -394,6 +394,8 @@ function createMockWorker(overrides: Partial<WorkerInfo> = {}): WorkerInfo {
id: 'w-test123',
status: 'active',
beadsCompleted: 5,
beadsSucceeded: 3,
beadsTimedOut: 2,
firstSeen: Date.now() - 60000,
lastActivity: Date.now(),
activeFiles: [],
@ -432,9 +434,9 @@ describe('TUI Enhanced Regression Tests', () => {
it('should snapshot worker grid with active workers', () => {
const workers = [
createMockWorker({ id: 'w-active123', status: 'active', beadsCompleted: 10 }),
createMockWorker({ id: 'w-idle456', status: 'idle', beadsCompleted: 5 }),
createMockWorker({ id: 'w-error789', status: 'error', beadsCompleted: 2 }),
createMockWorker({ id: 'w-active123', status: 'active', beadsCompleted: 10, beadsSucceeded: 3, beadsTimedOut: 2 }),
createMockWorker({ id: 'w-idle456', status: 'idle', beadsCompleted: 5, beadsSucceeded: 2, beadsTimedOut: 1 }),
createMockWorker({ id: 'w-error789', status: 'error', beadsCompleted: 2, beadsSucceeded: 0, beadsTimedOut: 2 }),
];
workers.forEach(w => {

View file

@ -254,6 +254,8 @@ function createMockWorker(overrides: Partial<WorkerInfo> = {}): WorkerInfo {
id: 'w-test123',
status: 'active',
beadsCompleted: 5,
beadsSucceeded: 3,
beadsTimedOut: 2,
firstSeen: Date.now() - 60000,
lastActivity: Date.now(),
activeFiles: [],
@ -1017,9 +1019,9 @@ describe('TUI Regression Tests', () => {
it('should render consistent worker grid content format', () => {
const workers = [
createMockWorker({ id: 'w-active123', status: 'active', beadsCompleted: 10 }),
createMockWorker({ id: 'w-idle456', status: 'idle', beadsCompleted: 5 }),
createMockWorker({ id: 'w-error789', status: 'error', beadsCompleted: 2 }),
createMockWorker({ id: 'w-active123', status: 'active', beadsCompleted: 10, beadsSucceeded: 3, beadsTimedOut: 2 }),
createMockWorker({ id: 'w-idle456', status: 'idle', beadsCompleted: 5, beadsSucceeded: 2, beadsTimedOut: 1 }),
createMockWorker({ id: 'w-error789', status: 'error', beadsCompleted: 2, beadsSucceeded: 0, beadsTimedOut: 2 }),
];
// Add events to create workers

View file

@ -11,6 +11,7 @@ const makeWorker = (overrides: Partial<WorkerInfo> = {}): WorkerInfo => ({
status: 'active',
beadsCompleted: 0, // All processed (including timed-out/deferred)
beadsSucceeded: 3, // Successful completions only
beadsTimedOut: 0, // Timed out or deferred
firstSeen: Date.now() - 5 * 60 * 1000,
lastActivity: Date.now(),
activeFiles: [],

View file

@ -296,6 +296,7 @@ function detectLongRunning(
const evidence = [
`Beads successfully completed: ${worker.beadsSucceeded}`,
`Beads processed (including timed-out/deferred): ${completed || 0}`,
`Beads timed out or deferred: ${worker.beadsTimedOut || 0}`,
`Total events in window: ${events.length}`,
];
@ -303,6 +304,8 @@ function detectLongRunning(
let reason: string;
if (completed > 0 && succeeded === 0) {
reason = `Running for ${minutes}m with ${completed} processed but 0 successful completions (all timed out/deferred)`;
} else if (completed > succeeded) {
reason = `Running for ${minutes}m with ${completed} processed but only ${succeeded} successful completion(s) (${worker.beadsTimedOut || 0} timed out/deferred)`;
} else {
reason = `Running for ${minutes}m with only ${succeeded} successful completion(s)`;
}

View file

@ -560,6 +560,9 @@ export interface WorkerInfo {
/** Total beads successfully completed (bead.completed events only, excludes timed-out/deferred releases) */
beadsSucceeded: number;
/** Total beads that timed out or were deferred (subset of beadsCompleted) */
beadsTimedOut: number;
/** First seen timestamp */
firstSeen: number;

File diff suppressed because one or more lines are too long