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.
- package/.env.example +1 -1
- package/agent/agent-pool.mjs +9 -2
- package/agent/agent-prompt-catalog.mjs +971 -0
- package/agent/agent-prompts.mjs +2 -970
- package/agent/agent-supervisor.mjs +119 -6
- package/agent/autofix-git.mjs +33 -0
- package/agent/autofix-prompts.mjs +151 -0
- package/agent/autofix.mjs +11 -175
- package/agent/bosun-skills.mjs +3 -2
- package/bosun.config.example.json +17 -0
- package/bosun.schema.json +87 -188
- package/cli.mjs +34 -1
- package/config/config-doctor.mjs +5 -250
- package/config/config-file-names.mjs +5 -0
- package/config/config.mjs +89 -493
- package/config/executor-config.mjs +493 -0
- package/config/repo-root.mjs +1 -2
- package/config/workspace-health.mjs +242 -0
- package/git/git-safety.mjs +15 -0
- package/github/github-oauth-portal.mjs +46 -0
- package/infra/library-manager-utils.mjs +22 -0
- package/infra/library-manager-well-known-sources.mjs +578 -0
- package/infra/library-manager.mjs +512 -1030
- package/infra/monitor.mjs +35 -9
- package/infra/session-tracker.mjs +10 -7
- package/kanban/kanban-adapter.mjs +17 -1
- package/lib/codebase-audit-manifests.mjs +117 -0
- package/lib/codebase-audit.mjs +18 -115
- package/package.json +18 -3
- package/server/setup-web-server.mjs +58 -5
- package/server/ui-server.mjs +1394 -79
- package/shell/codex-config-file.mjs +178 -0
- package/shell/codex-config.mjs +538 -575
- package/task/task-cli.mjs +54 -3
- package/task/task-executor.mjs +143 -13
- package/task/task-store.mjs +409 -1
- package/telegram/telegram-bot.mjs +127 -0
- package/tools/apply-pr-suggestions.mjs +401 -0
- package/tools/syntax-check.mjs +28 -9
- package/ui/app.js +3 -14
- package/ui/components/kanban-board.js +227 -4
- package/ui/components/session-list.js +85 -5
- package/ui/demo-defaults.js +338 -84
- package/ui/demo.html +155 -0
- package/ui/modules/session-api.js +96 -0
- package/ui/modules/settings-schema.js +1 -2
- package/ui/modules/state.js +43 -3
- package/ui/setup.html +4 -5
- package/ui/styles/components.css +58 -4
- package/ui/tabs/agents.js +12 -15
- package/ui/tabs/control.js +1 -0
- package/ui/tabs/library.js +484 -22
- package/ui/tabs/manual-flows.js +105 -29
- package/ui/tabs/tasks.js +848 -141
- package/ui/tabs/telemetry.js +129 -11
- package/ui/tabs/workflow-canvas-utils.mjs +130 -0
- package/ui/tabs/workflows.js +293 -23
- package/voice/voice-tool-definitions.mjs +757 -0
- package/voice/voice-tools.mjs +34 -778
- package/workflow/manual-flow-audit.mjs +165 -0
- package/workflow/manual-flows.mjs +164 -259
- package/workflow/workflow-engine.mjs +147 -58
- package/workflow/workflow-nodes/definitions.mjs +1207 -0
- package/workflow/workflow-nodes/transforms.mjs +612 -0
- package/workflow/workflow-nodes.mjs +358 -63
- package/workflow/workflow-templates.mjs +313 -191
- package/workflow-templates/_helpers.mjs +154 -0
- package/workflow-templates/agents.mjs +61 -4
- package/workflow-templates/code-quality.mjs +7 -7
- package/workflow-templates/github.mjs +20 -10
- package/workflow-templates/task-batch.mjs +44 -11
- package/workflow-templates/task-lifecycle.mjs +31 -6
- 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
|
package/task/task-executor.mjs
CHANGED
|
@@ -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
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
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
|
-
|
|
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
|
-
|