bosun 0.41.2 → 0.41.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/.env.example +1 -1
  2. package/agent/agent-pool.mjs +9 -2
  3. package/agent/agent-prompt-catalog.mjs +971 -0
  4. package/agent/agent-prompts.mjs +2 -970
  5. package/agent/agent-supervisor.mjs +119 -6
  6. package/agent/autofix-git.mjs +33 -0
  7. package/agent/autofix-prompts.mjs +151 -0
  8. package/agent/autofix.mjs +11 -175
  9. package/agent/bosun-skills.mjs +3 -2
  10. package/bosun.config.example.json +17 -0
  11. package/bosun.schema.json +87 -188
  12. package/cli.mjs +34 -1
  13. package/config/config-doctor.mjs +5 -250
  14. package/config/config-file-names.mjs +5 -0
  15. package/config/config.mjs +89 -493
  16. package/config/executor-config.mjs +493 -0
  17. package/config/repo-root.mjs +1 -2
  18. package/config/workspace-health.mjs +242 -0
  19. package/git/git-safety.mjs +15 -0
  20. package/github/github-oauth-portal.mjs +46 -0
  21. package/infra/library-manager-utils.mjs +22 -0
  22. package/infra/library-manager-well-known-sources.mjs +578 -0
  23. package/infra/library-manager.mjs +512 -1030
  24. package/infra/monitor.mjs +35 -9
  25. package/infra/session-tracker.mjs +10 -7
  26. package/kanban/kanban-adapter.mjs +17 -1
  27. package/lib/codebase-audit-manifests.mjs +117 -0
  28. package/lib/codebase-audit.mjs +18 -115
  29. package/package.json +18 -3
  30. package/server/setup-web-server.mjs +58 -5
  31. package/server/ui-server.mjs +1394 -79
  32. package/shell/codex-config-file.mjs +178 -0
  33. package/shell/codex-config.mjs +538 -575
  34. package/task/task-cli.mjs +54 -3
  35. package/task/task-executor.mjs +143 -13
  36. package/task/task-store.mjs +409 -1
  37. package/telegram/telegram-bot.mjs +127 -0
  38. package/tools/apply-pr-suggestions.mjs +401 -0
  39. package/tools/syntax-check.mjs +28 -9
  40. package/ui/app.js +3 -14
  41. package/ui/components/kanban-board.js +227 -4
  42. package/ui/components/session-list.js +85 -5
  43. package/ui/demo-defaults.js +338 -84
  44. package/ui/demo.html +155 -0
  45. package/ui/modules/session-api.js +96 -0
  46. package/ui/modules/settings-schema.js +1 -2
  47. package/ui/modules/state.js +43 -3
  48. package/ui/setup.html +4 -5
  49. package/ui/styles/components.css +58 -4
  50. package/ui/tabs/agents.js +12 -15
  51. package/ui/tabs/control.js +1 -0
  52. package/ui/tabs/library.js +484 -22
  53. package/ui/tabs/manual-flows.js +105 -29
  54. package/ui/tabs/tasks.js +848 -141
  55. package/ui/tabs/telemetry.js +129 -11
  56. package/ui/tabs/workflow-canvas-utils.mjs +130 -0
  57. package/ui/tabs/workflows.js +293 -23
  58. package/voice/voice-tool-definitions.mjs +757 -0
  59. package/voice/voice-tools.mjs +34 -778
  60. package/workflow/manual-flow-audit.mjs +165 -0
  61. package/workflow/manual-flows.mjs +164 -259
  62. package/workflow/workflow-engine.mjs +147 -58
  63. package/workflow/workflow-nodes/definitions.mjs +1207 -0
  64. package/workflow/workflow-nodes/transforms.mjs +612 -0
  65. package/workflow/workflow-nodes.mjs +358 -63
  66. package/workflow/workflow-templates.mjs +313 -191
  67. package/workflow-templates/_helpers.mjs +154 -0
  68. package/workflow-templates/agents.mjs +61 -4
  69. package/workflow-templates/code-quality.mjs +7 -7
  70. package/workflow-templates/github.mjs +20 -10
  71. package/workflow-templates/task-batch.mjs +44 -11
  72. package/workflow-templates/task-lifecycle.mjs +31 -6
  73. package/workspace/worktree-manager.mjs +277 -3
package/task/task-cli.mjs CHANGED
@@ -12,7 +12,7 @@
12
12
  * bosun task update <task-id> <json-patch>
13
13
  * bosun task update <task-id> --status todo --priority high
14
14
  * bosun task delete <task-id>
15
- * bosun task stats [--json]
15
+ * bosun task stats [--json] [--debug]
16
16
  * bosun task import <json-file>
17
17
  *
18
18
  * EXPORTS:
@@ -234,6 +234,12 @@ function hasFlag(args, flag) {
234
234
  return args.includes(flag);
235
235
  }
236
236
 
237
+ function isDebugModeEnabled(args = []) {
238
+ if (hasFlag(args, "--debug")) return true;
239
+ const envValue = String(process.env.BOSUN_DEBUG || "").trim().toLowerCase();
240
+ return ["1", "true", "yes", "on"].includes(envValue);
241
+ }
242
+
237
243
  // ── Programmatic API ──────────────────────────────────────────────────────────
238
244
 
239
245
  /**
@@ -529,6 +535,25 @@ function readRepoAreaLocksFromRuntimeState() {
529
535
  parsed?.repoAreaDispatchCycle && typeof parsed.repoAreaDispatchCycle === "object"
530
536
  ? parsed.repoAreaDispatchCycle
531
537
  : {};
538
+ const contentionEvents = Array.isArray(parsed?.repoAreaContentionEvents)
539
+ ? parsed.repoAreaContentionEvents
540
+ .slice(-60)
541
+ .map((event) => ({
542
+ at: event?.at ? String(event.at) : null,
543
+ taskId: normalizeTaskId(event?.taskId),
544
+ area: normalizeRepoAreaKey(event?.area),
545
+ waitMs: Math.max(0, Math.trunc(Number(event?.waitMs || 0))),
546
+ resolutionReason: normalizeRepoAreaResolutionReason(
547
+ event?.resolutionReason,
548
+ ),
549
+ }))
550
+ .filter((event) => event.taskId && event.area)
551
+ : [];
552
+ const contentionByReason = Object.create(null);
553
+ for (const event of contentionEvents) {
554
+ const reason = normalizeRepoAreaResolutionReason(event.resolutionReason);
555
+ contentionByReason[reason] = (contentionByReason[reason] || 0) + 1;
556
+ }
532
557
  const activeSignals = new Map();
533
558
  const activeCounts = new Map();
534
559
  for (const [taskId, slot] of Object.entries(runtimeSlots)) {
@@ -696,6 +721,16 @@ function readRepoAreaLocksFromRuntimeState() {
696
721
  ),
697
722
  waitSamples: areas.reduce((sum, area) => sum + (area.waitSamples || 0), 0),
698
723
  waitingTasks: areas.reduce((sum, area) => sum + (area.waitingTasks || 0), 0),
724
+ contentionEvents: contentionEvents.length,
725
+ },
726
+ contention: {
727
+ events: contentionEvents.length,
728
+ waitMsTotal: contentionEvents.reduce(
729
+ (sum, event) => sum + Math.max(0, Number(event?.waitMs || 0)),
730
+ 0,
731
+ ),
732
+ byReason: contentionByReason,
733
+ recent: contentionEvents.slice(-10),
699
734
  },
700
735
  dispatch: {
701
736
  cycles: Math.max(0, Math.trunc(Number(parsed?.repoAreaDispatchCycles || 0))),
@@ -761,6 +796,11 @@ function normalizeRepoAreaKey(value) {
761
796
  return String(value || "").trim().toLowerCase();
762
797
  }
763
798
 
799
+ function normalizeRepoAreaResolutionReason(value, fallback = "resolved") {
800
+ const normalized = normalizeRepoAreaKey(value);
801
+ return normalized || fallback;
802
+ }
803
+
764
804
  function normalizeRepoAreas(input) {
765
805
  if (!Array.isArray(input)) return [];
766
806
  return [...new Set(input.map((value) => normalizeRepoAreaKey(value)).filter(Boolean))];
@@ -1408,6 +1448,7 @@ async function cliDelete(args) {
1408
1448
 
1409
1449
  async function cliStats(args) {
1410
1450
  const stats = await taskStats();
1451
+ const debugMode = isDebugModeEnabled(args);
1411
1452
 
1412
1453
  if (hasFlag(args, "--json")) {
1413
1454
  console.log(JSON.stringify(stats, null, 2));
@@ -1422,13 +1463,15 @@ async function cliStats(args) {
1422
1463
  console.log(` Done: ${stats.done || 0}`);
1423
1464
  console.log(` Blocked: ${stats.blocked || 0}`);
1424
1465
  console.log(` Total: ${stats.total || 0}`);
1425
- if (stats.repoAreaLocks) {
1466
+ if (debugMode && stats.repoAreaLocks) {
1426
1467
  const lockState = stats.repoAreaLocks;
1427
1468
  const totals = lockState.totals || {};
1469
+ const contention = lockState.contention || {};
1428
1470
  console.log(`\n Repo Area Locks:`);
1429
1471
  console.log(` Dispatch Cycles: ${totals.dispatchCycles || lockState.dispatchCycles || 0}`);
1430
1472
  console.log(` Conflict Events: ${totals.conflictEvents || lockState.conflictEvents || 0}`);
1431
1473
  console.log(` Blocked Tracked: ${lockState.blockedTasksTracked || lockState.dispatch?.blockedTasksTracked || 0}`);
1474
+ console.log(` Contention Events: ${totals.contentionEvents || contention.events || 0}`);
1432
1475
  const totalWaitSamples = Number(totals.waitSamples || 0);
1433
1476
  const totalWaitMs = Number(totals.waitMsTotal || 0);
1434
1477
  const globalAvgWaitMs =
@@ -1451,6 +1494,14 @@ async function cliStats(args) {
1451
1494
  ` - ${area.area}: blocked=${area.blockedDispatches || 0}, selected=${area.selectedDispatches || 0}, limit=${area.effectiveLimit || 0}/${area.configuredLimit || lockState.configuredLimit || 0}, avgWaitMs=${Math.round(area.averageWaitMs || 0)}`,
1452
1495
  );
1453
1496
  }
1497
+ const recentEvents = Array.isArray(contention.recent)
1498
+ ? contention.recent.slice(-3)
1499
+ : [];
1500
+ for (const event of recentEvents) {
1501
+ console.log(
1502
+ ` - contention: area=${event.area || "unknown"}, waitMs=${Math.max(0, Math.trunc(Number(event.waitMs || 0)))}, reason=${normalizeRepoAreaResolutionReason(event.resolutionReason)}, task=${String(event.taskId || "").slice(0, 8)}`,
1503
+ );
1504
+ }
1454
1505
  }
1455
1506
  console.log("");
1456
1507
  }
@@ -1600,7 +1651,7 @@ function showTaskHelp() {
1600
1651
  get, show Show task details bosun task get --help
1601
1652
  update, edit Update task fields bosun task update --help
1602
1653
  delete, rm Delete a task bosun task delete --help
1603
- stats Aggregate statistics bosun task stats --json
1654
+ stats Aggregate statistics bosun task stats --json/--debug
1604
1655
  import Bulk import from JSON file bosun task import --help
1605
1656
 
1606
1657
  QUICK REFERENCE
@@ -98,6 +98,7 @@ import {
98
98
  initTaskClaims,
99
99
  claimTask,
100
100
  renewClaim,
101
+ getClaim,
101
102
  releaseTask as releaseTaskClaim,
102
103
  } from "./task-claims.mjs";
103
104
  import { initPresence, getPresenceState } from "../infra/presence.mjs";
@@ -120,6 +121,7 @@ const NO_COMMIT_MAX_COOLDOWN_MS = 2 * 60 * 60 * 1000;
120
121
  const CLAIM_CONFLICT_COMMENT_COOLDOWN_MS = 30 * 60 * 1000; // 30 minutes
121
122
  const REPO_AREA_SLOW_MERGE_LATENCY_MS = 4 * 60 * 60 * 1000;
122
123
  const REPO_AREA_VERY_SLOW_MERGE_LATENCY_MS = 8 * 60 * 60 * 1000;
124
+ const REPO_AREA_CONTENTION_EVENT_LIMIT = 60;
123
125
  const FATAL_CLAIM_RENEW_ERRORS = new Set([
124
126
  "task_claimed_by_different_instance",
125
127
  "claim_token_mismatch",
@@ -178,6 +180,24 @@ function transitionInternalTaskStatus(taskId, status, source) {
178
180
  return setInternalStatus(taskId, status, source);
179
181
  }
180
182
 
183
+ function isMatchingLocalClaimProcessAlive(ownerId, claim) {
184
+ if (!ownerId || !claim) return null;
185
+ const claimInstanceId = String(claim.instance_id || claim.instanceId || "");
186
+ if (!claimInstanceId || claimInstanceId !== String(ownerId)) return null;
187
+ const claimHost = String(claim?.metadata?.host || "").trim();
188
+ if (!claimHost || claimHost.toLowerCase() !== os.hostname().toLowerCase()) {
189
+ return null;
190
+ }
191
+ const claimPid = Number(claim?.metadata?.pid);
192
+ if (!Number.isFinite(claimPid) || claimPid <= 0) return null;
193
+ try {
194
+ process.kill(Math.floor(claimPid), 0);
195
+ return true;
196
+ } catch {
197
+ return false;
198
+ }
199
+ }
200
+
181
201
  function parseNumberEnv(name, fallback) {
182
202
  const value = Number(process.env[name]);
183
203
  return Number.isFinite(value) ? value : fallback;
@@ -313,6 +333,11 @@ function createEmptyRepoAreaTelemetryEntry() {
313
333
  return normalizeRepoAreaTelemetryEntry();
314
334
  }
315
335
 
336
+ function normalizeRepoAreaResolutionReason(value, fallback = "resolved") {
337
+ const normalized = normalizeRepoAreaKey(value);
338
+ return normalized || fallback;
339
+ }
340
+
316
341
  function averageNumbers(values = []) {
317
342
  if (!Array.isArray(values) || values.length === 0) return 0;
318
343
  const total = values.reduce((sum, value) => sum + Number(value || 0), 0);
@@ -2511,6 +2536,8 @@ class TaskExecutor {
2511
2536
  this._repoAreaTaskAreas = new Map();
2512
2537
  /** @type {Map<string, number>} */
2513
2538
  this._repoAreaTaskStartedAt = new Map();
2539
+ /** @type {Array<{ at: string, taskId: string, area: string, waitMs: number, resolutionReason: string }>} */
2540
+ this._repoAreaContentionEvents = [];
2514
2541
  this._repoAreaDispatchCycles = 0;
2515
2542
  this._repoAreaConflictCount = 0;
2516
2543
 
@@ -2852,6 +2879,22 @@ class TaskExecutor {
2852
2879
  if (!key || !startedAt) continue;
2853
2880
  this._repoAreaTaskStartedAt.set(key, startedAt);
2854
2881
  }
2882
+ this._repoAreaContentionEvents = Array.isArray(parsed?.repoAreaContentionEvents)
2883
+ ? parsed.repoAreaContentionEvents
2884
+ .slice(-REPO_AREA_CONTENTION_EVENT_LIMIT)
2885
+ .map((event) => ({
2886
+ at: event?.at
2887
+ ? String(event.at)
2888
+ : new Date().toISOString(),
2889
+ taskId: normalizeTaskIdKey(event?.taskId) || "",
2890
+ area: normalizeRepoAreaKey(event?.area) || "",
2891
+ waitMs: Math.max(0, Math.trunc(Number(event?.waitMs || 0))),
2892
+ resolutionReason: normalizeRepoAreaResolutionReason(
2893
+ event?.resolutionReason,
2894
+ ),
2895
+ }))
2896
+ .filter((event) => event.taskId && event.area)
2897
+ : [];
2855
2898
  this._repoAreaDispatchHistory = Array.isArray(parsed?.repoAreaDispatchHistory)
2856
2899
  ? parsed.repoAreaDispatchHistory
2857
2900
  .slice(-20)
@@ -3059,6 +3102,9 @@ class TaskExecutor {
3059
3102
  repoAreaBlockedTasks: Object.fromEntries(this._repoAreaBlockedTasks),
3060
3103
  repoAreaTaskAreas: Object.fromEntries(this._repoAreaTaskAreas),
3061
3104
  repoAreaTaskStartedAt: Object.fromEntries(this._repoAreaTaskStartedAt),
3105
+ repoAreaContentionEvents: this._repoAreaContentionEvents.slice(
3106
+ -REPO_AREA_CONTENTION_EVENT_LIMIT,
3107
+ ),
3062
3108
  repoAreaDispatchHistory: this._repoAreaDispatchHistory.slice(-20),
3063
3109
  repoAreaLockStatus: this._buildRepoAreaLockStatus(),
3064
3110
  slots,
@@ -3207,6 +3253,11 @@ class TaskExecutor {
3207
3253
  }
3208
3254
  }
3209
3255
 
3256
+ this._clearRepoAreaWaitsForTask(
3257
+ typeof taskOrTaskId === "string" ? { id: taskId, repo_areas: areas } : taskOrTaskId,
3258
+ now,
3259
+ "abandoned",
3260
+ );
3210
3261
  this._clearRepoAreaBlockedTask(taskId);
3211
3262
  if (isFailure || isSuccess) {
3212
3263
  this._repoAreaTaskStartedAt.delete(taskId);
@@ -3899,6 +3950,7 @@ class TaskExecutor {
3899
3950
  continue;
3900
3951
  }
3901
3952
 
3953
+ let hasStaleSharedClaim = false;
3902
3954
  if (SHARED_STATE_ENABLED) {
3903
3955
  try {
3904
3956
  const sharedState = await getSharedState(id, this.repoRoot);
@@ -3909,12 +3961,23 @@ class TaskExecutor {
3909
3961
  // block recovery re-dispatch. Removing the ownerId !== instanceId
3910
3962
  // guard ensures workflow-owned tasks (wf-<uuid> owners) are also
3911
3963
  // protected when action.claim_task IS used.
3912
- if (
3913
- ownerId &&
3914
- !isSharedHeartbeatStale(heartbeat, SHARED_STATE_STALE_THRESHOLD_MS)
3915
- ) {
3916
- skippedForActiveClaim++;
3917
- continue;
3964
+ if (ownerId) {
3965
+ const heartbeatIsFresh = !isSharedHeartbeatStale(
3966
+ heartbeat,
3967
+ SHARED_STATE_STALE_THRESHOLD_MS,
3968
+ );
3969
+ if (heartbeatIsFresh) {
3970
+ const claim = await getClaim(id).catch(() => null);
3971
+ const localClaimAlive = isMatchingLocalClaimProcessAlive(
3972
+ ownerId,
3973
+ claim,
3974
+ );
3975
+ if (localClaimAlive !== false) {
3976
+ skippedForActiveClaim++;
3977
+ continue;
3978
+ }
3979
+ }
3980
+ hasStaleSharedClaim = true;
3918
3981
  }
3919
3982
  } catch {
3920
3983
  /* best effort */
@@ -3996,6 +4059,27 @@ class TaskExecutor {
3996
4059
  skippedForActiveClaim++;
3997
4060
  continue;
3998
4061
  }
4062
+ if (hasStaleSharedClaim) {
4063
+ try {
4064
+ await transitionTaskStatus(id, "todo", {
4065
+ source: "task-executor-recovery-stale-workflow-claim",
4066
+ });
4067
+ } catch {
4068
+ /* best effort */
4069
+ }
4070
+ try {
4071
+ transitionInternalTaskStatus(
4072
+ id,
4073
+ "todo",
4074
+ "task-executor-recovery-stale-workflow-claim",
4075
+ );
4076
+ } catch {
4077
+ /* best effort */
4078
+ }
4079
+ this._removeRuntimeSlot(id);
4080
+ resetToTodo++;
4081
+ continue;
4082
+ }
3999
4083
  if (isFreshEnough) {
4000
4084
  skippedForActiveClaim++;
4001
4085
  continue;
@@ -4392,7 +4476,30 @@ class TaskExecutor {
4392
4476
  });
4393
4477
  }
4394
4478
 
4395
- _finalizeRepoAreaWait(taskId, area, now = Date.now()) {
4479
+ _recordRepoAreaContentionEvent(taskId, area, waitMs, resolutionReason) {
4480
+ const normalizedTaskId = normalizeTaskIdKey(taskId);
4481
+ const normalizedArea = normalizeRepoAreaKey(area);
4482
+ if (!normalizedTaskId || !normalizedArea) return;
4483
+ this._repoAreaContentionEvents.push({
4484
+ at: new Date().toISOString(),
4485
+ taskId: normalizedTaskId,
4486
+ area: normalizedArea,
4487
+ waitMs: Math.max(0, Math.trunc(Number(waitMs || 0))),
4488
+ resolutionReason: normalizeRepoAreaResolutionReason(resolutionReason),
4489
+ });
4490
+ if (this._repoAreaContentionEvents.length > REPO_AREA_CONTENTION_EVENT_LIMIT) {
4491
+ this._repoAreaContentionEvents = this._repoAreaContentionEvents.slice(
4492
+ -REPO_AREA_CONTENTION_EVENT_LIMIT,
4493
+ );
4494
+ }
4495
+ }
4496
+
4497
+ _finalizeRepoAreaWait(
4498
+ taskId,
4499
+ area,
4500
+ now = Date.now(),
4501
+ resolutionReason = "resolved",
4502
+ ) {
4396
4503
  const key = this._makeRepoAreaWaitKey(taskId, area);
4397
4504
  if (!key) return 0;
4398
4505
  const pending = this._repoAreaPendingWaits.get(key);
@@ -4405,6 +4512,12 @@ class TaskExecutor {
4405
4512
  metric.waitSamples += 1;
4406
4513
  metric.maxWaitMs = Math.max(metric.maxWaitMs, durationMs);
4407
4514
  }
4515
+ this._recordRepoAreaContentionEvent(
4516
+ pending.taskId,
4517
+ pending.area,
4518
+ durationMs,
4519
+ resolutionReason,
4520
+ );
4408
4521
  return durationMs;
4409
4522
  }
4410
4523
 
@@ -4419,16 +4532,16 @@ class TaskExecutor {
4419
4532
  !candidateIds.has(pending.taskId) &&
4420
4533
  !this._activeSlots.has(pending.taskId)
4421
4534
  ) {
4422
- this._finalizeRepoAreaWait(pending.taskId, pending.area, now);
4535
+ this._finalizeRepoAreaWait(pending.taskId, pending.area, now, "dequeued");
4423
4536
  }
4424
4537
  }
4425
4538
  }
4426
4539
 
4427
- _clearRepoAreaWaitsForTask(task, now = Date.now()) {
4428
- const taskId = normalizeTaskIdKey(task?.id || task?.task_id);
4540
+ _clearRepoAreaWaitsForTask(task, now = Date.now(), resolutionReason = "resolved") {
4541
+ const taskId = normalizeTaskIdKey(task?.id || task?.task_id || task?.taskId);
4429
4542
  if (!taskId) return;
4430
4543
  for (const area of this._extractTaskRepoAreas(task)) {
4431
- this._finalizeRepoAreaWait(taskId, area, now);
4544
+ this._finalizeRepoAreaWait(taskId, area, now, resolutionReason);
4432
4545
  }
4433
4546
  }
4434
4547
 
@@ -4626,6 +4739,14 @@ class TaskExecutor {
4626
4739
  lastSelectedAt: metric.lastSelectedAt,
4627
4740
  };
4628
4741
  });
4742
+ const contentionByReason = Object.create(null);
4743
+ for (const event of this._repoAreaContentionEvents) {
4744
+ const reason = normalizeRepoAreaResolutionReason(event?.resolutionReason);
4745
+ contentionByReason[reason] = (contentionByReason[reason] || 0) + 1;
4746
+ }
4747
+ const contentionEvents = this._repoAreaContentionEvents.slice(
4748
+ -REPO_AREA_CONTENTION_EVENT_LIMIT,
4749
+ );
4629
4750
 
4630
4751
  return {
4631
4752
  enabled: Number(this.repoAreaParallelLimit || 0) > 0,
@@ -4648,6 +4769,16 @@ class TaskExecutor {
4648
4769
  waitMsTotal: items.reduce((sum, item) => sum + item.waitMsTotal, 0),
4649
4770
  waitSamples: items.reduce((sum, item) => sum + item.waitSamples, 0),
4650
4771
  waitingTasks: items.reduce((sum, item) => sum + item.waitingTasks, 0),
4772
+ contentionEvents: contentionEvents.length,
4773
+ },
4774
+ contention: {
4775
+ events: contentionEvents.length,
4776
+ waitMsTotal: contentionEvents.reduce(
4777
+ (sum, event) => sum + Math.max(0, Number(event?.waitMs || 0)),
4778
+ 0,
4779
+ ),
4780
+ byReason: contentionByReason,
4781
+ recent: contentionEvents.slice(-10),
4651
4782
  },
4652
4783
  dispatch: {
4653
4784
  cycles: Math.max(0, Math.trunc(Number(this._repoAreaDispatchCycles || 0))),
@@ -4811,7 +4942,7 @@ class TaskExecutor {
4811
4942
  }
4812
4943
  this._rememberTaskRepoAreas(task, now);
4813
4944
  this._clearRepoAreaBlockedTask(task?.id || task?.task_id);
4814
- this._clearRepoAreaWaitsForTask(task, now);
4945
+ this._clearRepoAreaWaitsForTask(task, now, "selected");
4815
4946
  for (const area of areas) {
4816
4947
  repoAreaCounts.set(area, (repoAreaCounts.get(area) || 0) + 1);
4817
4948
  const metric = this._getRepoAreaLockMetric(area);
@@ -5662,4 +5793,3 @@ export function isExecutorDisabled() {
5662
5793
 
5663
5794
  export { TaskExecutor };
5664
5795
  export default TaskExecutor;
5665
-