bosun 0.40.2 → 0.40.3
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/cli.mjs +86 -67
- package/infra/monitor.mjs +8 -3
- package/infra/update-check.mjs +1 -3
- package/package.json +1 -1
- package/server/ui-server.mjs +117 -0
- package/task/task-executor.mjs +690 -6
- package/task/task-store.mjs +116 -1
- package/ui/demo.html +26 -1
- package/ui/styles/components.css +43 -3
- package/ui/tabs/tasks.js +387 -97
- package/workflow/workflow-engine.mjs +30 -5
- package/workflow/workflow-nodes.mjs +102 -2
- package/workspace/workspace-manager.mjs +14 -0
- package/workspace/worktree-manager.mjs +2 -2
package/task/task-executor.mjs
CHANGED
|
@@ -109,6 +109,8 @@ const MAX_NO_COMMIT_ATTEMPTS = 3; // Stop picking up a task after N consecutive
|
|
|
109
109
|
const NO_COMMIT_COOLDOWN_BASE_MS = 15 * 60 * 1000; // 15 minutes base cooldown for no-commit
|
|
110
110
|
const NO_COMMIT_MAX_COOLDOWN_MS = 2 * 60 * 60 * 1000;
|
|
111
111
|
const CLAIM_CONFLICT_COMMENT_COOLDOWN_MS = 30 * 60 * 1000; // 30 minutes
|
|
112
|
+
const REPO_AREA_SLOW_MERGE_LATENCY_MS = 4 * 60 * 60 * 1000;
|
|
113
|
+
const REPO_AREA_VERY_SLOW_MERGE_LATENCY_MS = 8 * 60 * 60 * 1000;
|
|
112
114
|
const FATAL_CLAIM_RENEW_ERRORS = new Set([
|
|
113
115
|
"task_claimed_by_different_instance",
|
|
114
116
|
"claim_token_mismatch",
|
|
@@ -244,6 +246,188 @@ function truncateUtf8Bytes(text, maxBytes = 12000) {
|
|
|
244
246
|
return `${best}${suffix}`;
|
|
245
247
|
}
|
|
246
248
|
|
|
249
|
+
function normalizeRepoAreaKey(value) {
|
|
250
|
+
return String(value || "").trim().toLowerCase();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function extractRepoAreasFromTaskLike(task) {
|
|
254
|
+
const values = [];
|
|
255
|
+
const pushValues = (input) => {
|
|
256
|
+
if (!input) return;
|
|
257
|
+
if (Array.isArray(input)) {
|
|
258
|
+
for (const entry of input) pushValues(entry);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const normalized = normalizeRepoAreaKey(input);
|
|
262
|
+
if (!normalized) return;
|
|
263
|
+
values.push(normalized);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
pushValues(task?.repo_areas);
|
|
267
|
+
pushValues(task?.repoAreas);
|
|
268
|
+
pushValues(task?.meta?.repo_areas);
|
|
269
|
+
pushValues(task?.meta?.repoAreas);
|
|
270
|
+
pushValues(task?.task?.repo_areas);
|
|
271
|
+
pushValues(task?.task?.repoAreas);
|
|
272
|
+
pushValues(task?.task?.meta?.repo_areas);
|
|
273
|
+
pushValues(task?.task?.meta?.repoAreas);
|
|
274
|
+
|
|
275
|
+
return [...new Set(values)].slice(0, 10);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function normalizeRepoAreaTelemetryEntry(raw = {}) {
|
|
279
|
+
const recentOutcomes = Array.isArray(raw?.recentOutcomes)
|
|
280
|
+
? raw.recentOutcomes
|
|
281
|
+
.map((value) => Number(value))
|
|
282
|
+
.filter((value) => value === 0 || value === 1)
|
|
283
|
+
.slice(-20)
|
|
284
|
+
: [];
|
|
285
|
+
const mergeLatencySamples = Array.isArray(raw?.mergeLatencySamples)
|
|
286
|
+
? raw.mergeLatencySamples
|
|
287
|
+
.map((value) => Number(value))
|
|
288
|
+
.filter((value) => Number.isFinite(value) && value >= 0)
|
|
289
|
+
.slice(-12)
|
|
290
|
+
: [];
|
|
291
|
+
return {
|
|
292
|
+
conflictCount: Math.max(0, Math.trunc(Number(raw?.conflictCount || 0))),
|
|
293
|
+
totalWaitMs: Math.max(0, Math.trunc(Number(raw?.totalWaitMs || 0))),
|
|
294
|
+
lastWaitMs: Math.max(0, Math.trunc(Number(raw?.lastWaitMs || 0))),
|
|
295
|
+
maxWaitMs: Math.max(0, Math.trunc(Number(raw?.maxWaitMs || 0))),
|
|
296
|
+
lastBlockedAt: Math.max(0, Math.trunc(Number(raw?.lastBlockedAt || 0))),
|
|
297
|
+
lastOutcomeAt: Math.max(0, Math.trunc(Number(raw?.lastOutcomeAt || 0))),
|
|
298
|
+
recentOutcomes,
|
|
299
|
+
mergeLatencySamples,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function createEmptyRepoAreaTelemetryEntry() {
|
|
304
|
+
return normalizeRepoAreaTelemetryEntry();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function averageNumbers(values = []) {
|
|
308
|
+
if (!Array.isArray(values) || values.length === 0) return 0;
|
|
309
|
+
const total = values.reduce((sum, value) => sum + Number(value || 0), 0);
|
|
310
|
+
return total / values.length;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function buildRepoAreaAdaptiveSignals(entry, baseLimit) {
|
|
314
|
+
const normalizedBase = Number(baseLimit || 0);
|
|
315
|
+
const outcomes = Array.isArray(entry?.recentOutcomes) ? entry.recentOutcomes : [];
|
|
316
|
+
const failures = outcomes.reduce((sum, value) => sum + (Number(value) > 0 ? 1 : 0), 0);
|
|
317
|
+
const samples = outcomes.length;
|
|
318
|
+
const failureRate = samples > 0 ? failures / samples : 0;
|
|
319
|
+
const mergeLatencyAvgMs = averageNumbers(entry?.mergeLatencySamples || []);
|
|
320
|
+
const adaptiveReasons = [];
|
|
321
|
+
if (samples >= 4 && failureRate >= 0.5) adaptiveReasons.push("failure_rate");
|
|
322
|
+
if (
|
|
323
|
+
(entry?.mergeLatencySamples?.length || 0) >= 3 &&
|
|
324
|
+
mergeLatencyAvgMs >= REPO_AREA_SLOW_MERGE_LATENCY_MS
|
|
325
|
+
) {
|
|
326
|
+
adaptiveReasons.push("merge_latency");
|
|
327
|
+
}
|
|
328
|
+
const adaptivePenalty =
|
|
329
|
+
normalizedBase > 1 && adaptiveReasons.length > 0 ? 1 : 0;
|
|
330
|
+
const effectiveLimit =
|
|
331
|
+
normalizedBase > 0
|
|
332
|
+
? Math.max(1, normalizedBase - adaptivePenalty)
|
|
333
|
+
: 0;
|
|
334
|
+
return {
|
|
335
|
+
recentSamples: samples,
|
|
336
|
+
failureRate,
|
|
337
|
+
mergeLatencyAvgMs,
|
|
338
|
+
adaptivePenalty,
|
|
339
|
+
adaptiveReasons,
|
|
340
|
+
effectiveLimit,
|
|
341
|
+
severeMergeLatency:
|
|
342
|
+
(entry?.mergeLatencySamples?.length || 0) >= 3 &&
|
|
343
|
+
mergeLatencyAvgMs >= REPO_AREA_VERY_SLOW_MERGE_LATENCY_MS,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function buildRepoAreaWaitingCounts(blockedTasks = new Map()) {
|
|
348
|
+
const waitingCounts = new Map();
|
|
349
|
+
for (const blocked of blockedTasks.values()) {
|
|
350
|
+
const areas = Array.isArray(blocked?.areas) ? blocked.areas : [];
|
|
351
|
+
for (const area of areas) {
|
|
352
|
+
const areaKey = normalizeRepoAreaKey(area);
|
|
353
|
+
if (!areaKey) continue;
|
|
354
|
+
waitingCounts.set(areaKey, (waitingCounts.get(areaKey) || 0) + 1);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return waitingCounts;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function buildRepoAreaLockStatusSnapshot({
|
|
361
|
+
repoAreaParallelLimit = 0,
|
|
362
|
+
activeSlots = [],
|
|
363
|
+
telemetryByArea = new Map(),
|
|
364
|
+
blockedTasks = new Map(),
|
|
365
|
+
dispatchCycles = 0,
|
|
366
|
+
totalConflicts = 0,
|
|
367
|
+
} = {}) {
|
|
368
|
+
const baseLimit = Math.max(0, Math.trunc(Number(repoAreaParallelLimit || 0)));
|
|
369
|
+
const activeCounts = new Map();
|
|
370
|
+
for (const slot of activeSlots) {
|
|
371
|
+
const areas = extractRepoAreasFromTaskLike(slot);
|
|
372
|
+
for (const area of areas) {
|
|
373
|
+
activeCounts.set(area, (activeCounts.get(area) || 0) + 1);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const waitingCounts = buildRepoAreaWaitingCounts(blockedTasks);
|
|
378
|
+
const allAreas = new Set([
|
|
379
|
+
...activeCounts.keys(),
|
|
380
|
+
...waitingCounts.keys(),
|
|
381
|
+
...telemetryByArea.keys(),
|
|
382
|
+
]);
|
|
383
|
+
|
|
384
|
+
const areas = {};
|
|
385
|
+
const hotAreas = [];
|
|
386
|
+
for (const area of Array.from(allAreas).sort()) {
|
|
387
|
+
const telemetry = normalizeRepoAreaTelemetryEntry(telemetryByArea.get(area));
|
|
388
|
+
const adaptive = buildRepoAreaAdaptiveSignals(telemetry, baseLimit);
|
|
389
|
+
const snapshot = {
|
|
390
|
+
baseLimit,
|
|
391
|
+
effectiveLimit: adaptive.effectiveLimit,
|
|
392
|
+
active: activeCounts.get(area) || 0,
|
|
393
|
+
waiting: waitingCounts.get(area) || 0,
|
|
394
|
+
conflictCount: telemetry.conflictCount,
|
|
395
|
+
totalWaitMs: telemetry.totalWaitMs,
|
|
396
|
+
lastWaitMs: telemetry.lastWaitMs,
|
|
397
|
+
maxWaitMs: telemetry.maxWaitMs,
|
|
398
|
+
failureRate: adaptive.failureRate,
|
|
399
|
+
mergeLatencyAvgMs: adaptive.mergeLatencyAvgMs,
|
|
400
|
+
recentSamples: adaptive.recentSamples,
|
|
401
|
+
adaptivePenalty: adaptive.adaptivePenalty,
|
|
402
|
+
adaptiveReasons: adaptive.adaptiveReasons,
|
|
403
|
+
severeMergeLatency: adaptive.severeMergeLatency,
|
|
404
|
+
lastBlockedAt: telemetry.lastBlockedAt,
|
|
405
|
+
lastOutcomeAt: telemetry.lastOutcomeAt,
|
|
406
|
+
};
|
|
407
|
+
areas[area] = snapshot;
|
|
408
|
+
hotAreas.push({ area, ...snapshot });
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
hotAreas.sort((a, b) => {
|
|
412
|
+
if (b.waiting !== a.waiting) return b.waiting - a.waiting;
|
|
413
|
+
if (b.adaptivePenalty !== a.adaptivePenalty) {
|
|
414
|
+
return b.adaptivePenalty - a.adaptivePenalty;
|
|
415
|
+
}
|
|
416
|
+
if (b.conflictCount !== a.conflictCount) return b.conflictCount - a.conflictCount;
|
|
417
|
+
return a.area.localeCompare(b.area);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
enabled: baseLimit > 0,
|
|
422
|
+
baseLimit,
|
|
423
|
+
dispatchCycles: Math.max(0, Math.trunc(Number(dispatchCycles || 0))),
|
|
424
|
+
totalConflicts: Math.max(0, Math.trunc(Number(totalConflicts || 0))),
|
|
425
|
+
waitingTasks: blockedTasks.size,
|
|
426
|
+
areas,
|
|
427
|
+
hotAreas: hotAreas.slice(0, 5),
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
247
431
|
function isBosunStateComment(text) {
|
|
248
432
|
const raw = String(text || "").toLowerCase();
|
|
249
433
|
if (!raw) return false;
|
|
@@ -2095,6 +2279,20 @@ class TaskExecutor {
|
|
|
2095
2279
|
this._noCommitCounts = new Map();
|
|
2096
2280
|
/** @type {Map<string, number>} taskId → skip-until timestamp */
|
|
2097
2281
|
this._skipUntil = new Map();
|
|
2282
|
+
/** @type {Map<string, { conflicts: number, blockedDispatches: number, selectedDispatches: number, waitMsTotal: number, waitSamples: number, maxWaitMs: number, lastConflictAt: string|null, lastSelectedAt: string|null }>} */
|
|
2283
|
+
this._repoAreaLockMetrics = new Map();
|
|
2284
|
+
/** @type {Map<string, { taskId: string, area: string, startedAt: number }>} */
|
|
2285
|
+
this._repoAreaPendingWaits = new Map();
|
|
2286
|
+
this._repoAreaDispatchCycle = {
|
|
2287
|
+
cycle: 0,
|
|
2288
|
+
at: null,
|
|
2289
|
+
candidateCount: 0,
|
|
2290
|
+
remaining: 0,
|
|
2291
|
+
selectedCount: 0,
|
|
2292
|
+
blockedTasks: 0,
|
|
2293
|
+
blockedByArea: {},
|
|
2294
|
+
saturatedAreas: [],
|
|
2295
|
+
};
|
|
2098
2296
|
|
|
2099
2297
|
// Track tasks that have already been completed with a PR (prevents re-dispatch loop)
|
|
2100
2298
|
/** @type {Set<string>} taskId set */
|
|
@@ -2119,6 +2317,16 @@ class TaskExecutor {
|
|
|
2119
2317
|
/** @type {Map<string, { taskId: string, taskTitle: string, branch: string, sdk: string, model: string, attempt: number, startedAt: number, agentInstanceId: number|null, status: string, updatedAt: number }>} */
|
|
2120
2318
|
this._slotRuntimeState = new Map();
|
|
2121
2319
|
this._nextAgentInstanceId = 1;
|
|
2320
|
+
/** @type {Map<string, ReturnType<typeof normalizeRepoAreaTelemetryEntry>>} */
|
|
2321
|
+
this._repoAreaTelemetry = new Map();
|
|
2322
|
+
/** @type {Map<string, { blockedAt: number, lastObservedWaitMs: number, areas: string[] }>} */
|
|
2323
|
+
this._repoAreaBlockedTasks = new Map();
|
|
2324
|
+
/** @type {Map<string, string[]>} */
|
|
2325
|
+
this._repoAreaTaskAreas = new Map();
|
|
2326
|
+
/** @type {Map<string, number>} */
|
|
2327
|
+
this._repoAreaTaskStartedAt = new Map();
|
|
2328
|
+
this._repoAreaDispatchCycles = 0;
|
|
2329
|
+
this._repoAreaConflictCount = 0;
|
|
2122
2330
|
|
|
2123
2331
|
// Repo context cache (AGENTS.md, copilot-instructions.md)
|
|
2124
2332
|
this._contextCache = null;
|
|
@@ -2251,6 +2459,57 @@ class TaskExecutor {
|
|
|
2251
2459
|
this._nextAgentInstanceId = Math.floor(nextId);
|
|
2252
2460
|
}
|
|
2253
2461
|
|
|
2462
|
+
this._repoAreaDispatchCycles = Math.max(
|
|
2463
|
+
0,
|
|
2464
|
+
Math.trunc(Number(parsed?.repoAreaDispatchCycles || 0)),
|
|
2465
|
+
);
|
|
2466
|
+
this._repoAreaConflictCount = Math.max(
|
|
2467
|
+
0,
|
|
2468
|
+
Math.trunc(Number(parsed?.repoAreaConflictCount || 0)),
|
|
2469
|
+
);
|
|
2470
|
+
this._repoAreaTelemetry.clear();
|
|
2471
|
+
for (const [area, raw] of Object.entries(parsed?.repoAreaTelemetry || {})) {
|
|
2472
|
+
const areaKey = normalizeRepoAreaKey(area);
|
|
2473
|
+
if (!areaKey) continue;
|
|
2474
|
+
this._repoAreaTelemetry.set(areaKey, normalizeRepoAreaTelemetryEntry(raw));
|
|
2475
|
+
}
|
|
2476
|
+
this._repoAreaBlockedTasks.clear();
|
|
2477
|
+
for (const [taskId, raw] of Object.entries(parsed?.repoAreaBlockedTasks || {})) {
|
|
2478
|
+
const key = normalizeTaskIdKey(taskId);
|
|
2479
|
+
if (!key) continue;
|
|
2480
|
+
const blockedAt = Math.max(0, Math.trunc(Number(raw?.blockedAt || 0)));
|
|
2481
|
+
const lastObservedWaitMs = Math.max(
|
|
2482
|
+
0,
|
|
2483
|
+
Math.trunc(Number(raw?.lastObservedWaitMs || 0)),
|
|
2484
|
+
);
|
|
2485
|
+
const areas = Array.isArray(raw?.areas)
|
|
2486
|
+
? raw.areas.map((area) => normalizeRepoAreaKey(area)).filter(Boolean)
|
|
2487
|
+
: [];
|
|
2488
|
+
if (!blockedAt || areas.length === 0) continue;
|
|
2489
|
+
this._repoAreaBlockedTasks.set(key, {
|
|
2490
|
+
blockedAt,
|
|
2491
|
+
lastObservedWaitMs,
|
|
2492
|
+
areas: [...new Set(areas)],
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2495
|
+
this._repoAreaTaskAreas.clear();
|
|
2496
|
+
for (const [taskId, rawAreas] of Object.entries(parsed?.repoAreaTaskAreas || {})) {
|
|
2497
|
+
const key = normalizeTaskIdKey(taskId);
|
|
2498
|
+
if (!key) continue;
|
|
2499
|
+
const areas = Array.isArray(rawAreas)
|
|
2500
|
+
? rawAreas.map((area) => normalizeRepoAreaKey(area)).filter(Boolean)
|
|
2501
|
+
: [];
|
|
2502
|
+
if (areas.length === 0) continue;
|
|
2503
|
+
this._repoAreaTaskAreas.set(key, [...new Set(areas)]);
|
|
2504
|
+
}
|
|
2505
|
+
this._repoAreaTaskStartedAt.clear();
|
|
2506
|
+
for (const [taskId, rawStartedAt] of Object.entries(parsed?.repoAreaTaskStartedAt || {})) {
|
|
2507
|
+
const key = normalizeTaskIdKey(taskId);
|
|
2508
|
+
const startedAt = Math.max(0, Math.trunc(Number(rawStartedAt || 0)));
|
|
2509
|
+
if (!key || !startedAt) continue;
|
|
2510
|
+
this._repoAreaTaskStartedAt.set(key, startedAt);
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2254
2513
|
const slots = parsed?.slots || {};
|
|
2255
2514
|
let restored = 0;
|
|
2256
2515
|
for (const [taskId, entry] of Object.entries(slots)) {
|
|
@@ -2325,6 +2584,12 @@ class TaskExecutor {
|
|
|
2325
2584
|
pauseUntil: this._pauseUntil,
|
|
2326
2585
|
pauseReason: this._pauseReason,
|
|
2327
2586
|
nextAgentInstanceId: this._nextAgentInstanceId,
|
|
2587
|
+
repoAreaDispatchCycles: this._repoAreaDispatchCycles,
|
|
2588
|
+
repoAreaConflictCount: this._repoAreaConflictCount,
|
|
2589
|
+
repoAreaTelemetry: Object.fromEntries(this._repoAreaTelemetry),
|
|
2590
|
+
repoAreaBlockedTasks: Object.fromEntries(this._repoAreaBlockedTasks),
|
|
2591
|
+
repoAreaTaskAreas: Object.fromEntries(this._repoAreaTaskAreas),
|
|
2592
|
+
repoAreaTaskStartedAt: Object.fromEntries(this._repoAreaTaskStartedAt),
|
|
2328
2593
|
slots,
|
|
2329
2594
|
savedAt: new Date().toISOString(),
|
|
2330
2595
|
},
|
|
@@ -2362,6 +2627,132 @@ class TaskExecutor {
|
|
|
2362
2627
|
this._saveRuntimeState();
|
|
2363
2628
|
}
|
|
2364
2629
|
|
|
2630
|
+
_ensureRepoAreaTelemetry(areaKey) {
|
|
2631
|
+
const normalized = normalizeRepoAreaKey(areaKey);
|
|
2632
|
+
if (!normalized) return null;
|
|
2633
|
+
let entry = this._repoAreaTelemetry.get(normalized);
|
|
2634
|
+
if (!entry) {
|
|
2635
|
+
entry = createEmptyRepoAreaTelemetryEntry();
|
|
2636
|
+
this._repoAreaTelemetry.set(normalized, entry);
|
|
2637
|
+
}
|
|
2638
|
+
return entry;
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
_rememberTaskRepoAreas(task, now = Date.now()) {
|
|
2642
|
+
const taskId = normalizeTaskIdKey(task?.id || task?.taskId || task?.task_id);
|
|
2643
|
+
if (!taskId) return [];
|
|
2644
|
+
const areas = extractRepoAreasFromTaskLike(task);
|
|
2645
|
+
if (areas.length > 0) {
|
|
2646
|
+
this._repoAreaTaskAreas.set(taskId, areas);
|
|
2647
|
+
if (!this._repoAreaTaskStartedAt.has(taskId)) {
|
|
2648
|
+
this._repoAreaTaskStartedAt.set(taskId, now);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
return areas;
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
_clearRepoAreaBlockedTask(taskId) {
|
|
2655
|
+
const key = normalizeTaskIdKey(taskId);
|
|
2656
|
+
if (!key) return;
|
|
2657
|
+
this._repoAreaBlockedTasks.delete(key);
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
_recordRepoAreaConflict(task, blockedAreas, now = Date.now()) {
|
|
2661
|
+
const taskId = normalizeTaskIdKey(task?.id || task?.taskId || task?.task_id);
|
|
2662
|
+
if (!taskId || !Array.isArray(blockedAreas) || blockedAreas.length === 0) return;
|
|
2663
|
+
const normalizedAreas = [...new Set(blockedAreas.map((area) => normalizeRepoAreaKey(area)).filter(Boolean))];
|
|
2664
|
+
if (normalizedAreas.length === 0) return;
|
|
2665
|
+
const current = this._repoAreaBlockedTasks.get(taskId) || {
|
|
2666
|
+
blockedAt: now,
|
|
2667
|
+
lastObservedWaitMs: 0,
|
|
2668
|
+
areas: normalizedAreas,
|
|
2669
|
+
};
|
|
2670
|
+
current.areas = normalizedAreas;
|
|
2671
|
+
const waitMs = Math.max(0, now - current.blockedAt);
|
|
2672
|
+
const waitDelta = Math.max(0, waitMs - Number(current.lastObservedWaitMs || 0));
|
|
2673
|
+
current.lastObservedWaitMs = waitMs;
|
|
2674
|
+
this._repoAreaBlockedTasks.set(taskId, current);
|
|
2675
|
+
this._repoAreaConflictCount += 1;
|
|
2676
|
+
for (const areaKey of normalizedAreas) {
|
|
2677
|
+
const entry = this._ensureRepoAreaTelemetry(areaKey);
|
|
2678
|
+
if (!entry) continue;
|
|
2679
|
+
entry.conflictCount += 1;
|
|
2680
|
+
entry.totalWaitMs += waitDelta;
|
|
2681
|
+
entry.lastWaitMs = waitMs;
|
|
2682
|
+
entry.maxWaitMs = Math.max(entry.maxWaitMs, waitMs);
|
|
2683
|
+
entry.lastBlockedAt = now;
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
_getEffectiveRepoAreaLimit(areaKey) {
|
|
2688
|
+
const baseLimit = Number(this.repoAreaParallelLimit || 0);
|
|
2689
|
+
if (!Number.isFinite(baseLimit) || baseLimit <= 0) return 0;
|
|
2690
|
+
const entry = this._repoAreaTelemetry.get(normalizeRepoAreaKey(areaKey));
|
|
2691
|
+
return buildRepoAreaAdaptiveSignals(entry, baseLimit).effectiveLimit;
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
noteRepoAreaOutcome(taskOrTaskId, outcome = {}) {
|
|
2695
|
+
const taskId = normalizeTaskIdKey(
|
|
2696
|
+
typeof taskOrTaskId === "string"
|
|
2697
|
+
? taskOrTaskId
|
|
2698
|
+
: taskOrTaskId?.id || taskOrTaskId?.taskId || taskOrTaskId?.task_id,
|
|
2699
|
+
);
|
|
2700
|
+
if (!taskId) return false;
|
|
2701
|
+
|
|
2702
|
+
const areas =
|
|
2703
|
+
typeof taskOrTaskId === "string"
|
|
2704
|
+
? this._repoAreaTaskAreas.get(taskId) || []
|
|
2705
|
+
: this._rememberTaskRepoAreas(taskOrTaskId);
|
|
2706
|
+
if (!Array.isArray(areas) || areas.length === 0) return false;
|
|
2707
|
+
|
|
2708
|
+
const now = Date.now();
|
|
2709
|
+
const status = normalizeRepoAreaKey(outcome.status);
|
|
2710
|
+
const isFailure =
|
|
2711
|
+
outcome.failed === true ||
|
|
2712
|
+
["blocked", "failed", "error", "errored"].includes(status);
|
|
2713
|
+
const isSuccess =
|
|
2714
|
+
outcome.failed !== true &&
|
|
2715
|
+
["done", "inreview", "complete", "completed", "merged", "success"].includes(status);
|
|
2716
|
+
let mergeLatencyMs = Number(outcome.mergeLatencyMs || 0);
|
|
2717
|
+
if ((!Number.isFinite(mergeLatencyMs) || mergeLatencyMs <= 0) && isSuccess) {
|
|
2718
|
+
const startedAt = Number(this._repoAreaTaskStartedAt.get(taskId) || 0);
|
|
2719
|
+
if (startedAt > 0) mergeLatencyMs = Math.max(0, now - startedAt);
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
for (const areaKey of areas) {
|
|
2723
|
+
const entry = this._ensureRepoAreaTelemetry(areaKey);
|
|
2724
|
+
if (!entry) continue;
|
|
2725
|
+
if (isFailure || isSuccess) {
|
|
2726
|
+
entry.recentOutcomes.push(isFailure ? 1 : 0);
|
|
2727
|
+
entry.recentOutcomes = entry.recentOutcomes.slice(-20);
|
|
2728
|
+
entry.lastOutcomeAt = now;
|
|
2729
|
+
}
|
|
2730
|
+
if (Number.isFinite(mergeLatencyMs) && mergeLatencyMs > 0) {
|
|
2731
|
+
entry.mergeLatencySamples.push(Math.trunc(mergeLatencyMs));
|
|
2732
|
+
entry.mergeLatencySamples = entry.mergeLatencySamples.slice(-12);
|
|
2733
|
+
entry.lastOutcomeAt = now;
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
this._clearRepoAreaBlockedTask(taskId);
|
|
2738
|
+
if (isFailure || isSuccess) {
|
|
2739
|
+
this._repoAreaTaskStartedAt.delete(taskId);
|
|
2740
|
+
}
|
|
2741
|
+
this._saveRuntimeState();
|
|
2742
|
+
return true;
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
_buildRepoAreaLockStatus() {
|
|
2746
|
+
return buildRepoAreaLockStatusSnapshot({
|
|
2747
|
+
repoAreaParallelLimit: this.repoAreaParallelLimit,
|
|
2748
|
+
activeSlots: Array.from(this._activeSlots.values()),
|
|
2749
|
+
telemetryByArea: this._repoAreaTelemetry,
|
|
2750
|
+
blockedTasks: this._repoAreaBlockedTasks,
|
|
2751
|
+
dispatchCycles: this._repoAreaDispatchCycles,
|
|
2752
|
+
totalConflicts: this._repoAreaConflictCount,
|
|
2753
|
+
});
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2365
2756
|
/**
|
|
2366
2757
|
* Delete runtime state for a task slot after completion/failure reset.
|
|
2367
2758
|
* @param {string} taskId
|
|
@@ -3182,6 +3573,7 @@ class TaskExecutor {
|
|
|
3182
3573
|
maxRetries: this.maxRetries,
|
|
3183
3574
|
projectId: this._resolvedProjectId || this.projectId || null,
|
|
3184
3575
|
backlogReplenishment: { ...this._backlogReplenishment },
|
|
3576
|
+
repoAreaLocks: this._buildRepoAreaLockStatus(),
|
|
3185
3577
|
projectRequirements: { ...this._projectRequirements },
|
|
3186
3578
|
};
|
|
3187
3579
|
}
|
|
@@ -3428,6 +3820,253 @@ class TaskExecutor {
|
|
|
3428
3820
|
}
|
|
3429
3821
|
return counts;
|
|
3430
3822
|
}
|
|
3823
|
+
|
|
3824
|
+
_getRepoAreaLockMetric(area) {
|
|
3825
|
+
if (!area) return null;
|
|
3826
|
+
if (!this._repoAreaLockMetrics.has(area)) {
|
|
3827
|
+
this._repoAreaLockMetrics.set(area, {
|
|
3828
|
+
conflicts: 0,
|
|
3829
|
+
blockedDispatches: 0,
|
|
3830
|
+
selectedDispatches: 0,
|
|
3831
|
+
waitMsTotal: 0,
|
|
3832
|
+
waitSamples: 0,
|
|
3833
|
+
maxWaitMs: 0,
|
|
3834
|
+
lastConflictAt: null,
|
|
3835
|
+
lastSelectedAt: null,
|
|
3836
|
+
});
|
|
3837
|
+
}
|
|
3838
|
+
return this._repoAreaLockMetrics.get(area);
|
|
3839
|
+
}
|
|
3840
|
+
|
|
3841
|
+
_makeRepoAreaWaitKey(taskId, area) {
|
|
3842
|
+
const normalizedTaskId = normalizeTaskIdKey(taskId);
|
|
3843
|
+
if (!normalizedTaskId || !area) return "";
|
|
3844
|
+
return `${normalizedTaskId}::${area}`;
|
|
3845
|
+
}
|
|
3846
|
+
|
|
3847
|
+
_beginRepoAreaWait(taskId, area, now = Date.now()) {
|
|
3848
|
+
const key = this._makeRepoAreaWaitKey(taskId, area);
|
|
3849
|
+
if (!key || this._repoAreaPendingWaits.has(key)) return;
|
|
3850
|
+
this._repoAreaPendingWaits.set(key, {
|
|
3851
|
+
taskId: normalizeTaskIdKey(taskId),
|
|
3852
|
+
area,
|
|
3853
|
+
startedAt: now,
|
|
3854
|
+
});
|
|
3855
|
+
}
|
|
3856
|
+
|
|
3857
|
+
_finalizeRepoAreaWait(taskId, area, now = Date.now()) {
|
|
3858
|
+
const key = this._makeRepoAreaWaitKey(taskId, area);
|
|
3859
|
+
if (!key) return 0;
|
|
3860
|
+
const pending = this._repoAreaPendingWaits.get(key);
|
|
3861
|
+
if (!pending) return 0;
|
|
3862
|
+
this._repoAreaPendingWaits.delete(key);
|
|
3863
|
+
const durationMs = Math.max(0, now - Number(pending.startedAt || now));
|
|
3864
|
+
const metric = this._getRepoAreaLockMetric(area);
|
|
3865
|
+
if (metric && durationMs > 0) {
|
|
3866
|
+
metric.waitMsTotal += durationMs;
|
|
3867
|
+
metric.waitSamples += 1;
|
|
3868
|
+
metric.maxWaitMs = Math.max(metric.maxWaitMs, durationMs);
|
|
3869
|
+
}
|
|
3870
|
+
return durationMs;
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3873
|
+
_pruneRepoAreaPendingWaits(candidates, now = Date.now()) {
|
|
3874
|
+
const candidateIds = new Set(
|
|
3875
|
+
(Array.isArray(candidates) ? candidates : [])
|
|
3876
|
+
.map((task) => normalizeTaskIdKey(task?.id || task?.task_id))
|
|
3877
|
+
.filter(Boolean),
|
|
3878
|
+
);
|
|
3879
|
+
for (const pending of Array.from(this._repoAreaPendingWaits.values())) {
|
|
3880
|
+
if (
|
|
3881
|
+
!candidateIds.has(pending.taskId) &&
|
|
3882
|
+
!this._activeSlots.has(pending.taskId)
|
|
3883
|
+
) {
|
|
3884
|
+
this._finalizeRepoAreaWait(pending.taskId, pending.area, now);
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
|
|
3889
|
+
_clearRepoAreaWaitsForTask(task, now = Date.now()) {
|
|
3890
|
+
const taskId = normalizeTaskIdKey(task?.id || task?.task_id);
|
|
3891
|
+
if (!taskId) return;
|
|
3892
|
+
for (const area of this._extractTaskRepoAreas(task)) {
|
|
3893
|
+
this._finalizeRepoAreaWait(taskId, area, now);
|
|
3894
|
+
}
|
|
3895
|
+
}
|
|
3896
|
+
|
|
3897
|
+
_buildActiveRepoAreaSignals(now = Date.now()) {
|
|
3898
|
+
const signals = new Map();
|
|
3899
|
+
for (const slot of this._activeSlots.values()) {
|
|
3900
|
+
const areas = this._extractTaskRepoAreas(slot);
|
|
3901
|
+
if (areas.length === 0) continue;
|
|
3902
|
+
const startedAt = Number(slot?.startedAt || 0);
|
|
3903
|
+
const activeAgeMs = startedAt > 0 ? Math.max(0, now - startedAt) : 0;
|
|
3904
|
+
const mergeLatencyMs = Number(
|
|
3905
|
+
slot?.mergeLatencyMs || slot?.mergeLatency || 0,
|
|
3906
|
+
);
|
|
3907
|
+
const latencySampleMs = mergeLatencyMs > 0 ? mergeLatencyMs : activeAgeMs;
|
|
3908
|
+
const attempt = Number(slot?.attempt || 0);
|
|
3909
|
+
const status = String(slot?.status || "").trim().toLowerCase();
|
|
3910
|
+
for (const area of areas) {
|
|
3911
|
+
let signal = signals.get(area);
|
|
3912
|
+
if (!signal) {
|
|
3913
|
+
signal = {
|
|
3914
|
+
active: 0,
|
|
3915
|
+
retrying: 0,
|
|
3916
|
+
mergeLatencyMsTotal: 0,
|
|
3917
|
+
mergeLatencySamples: 0,
|
|
3918
|
+
maxMergeLatencyMs: 0,
|
|
3919
|
+
};
|
|
3920
|
+
signals.set(area, signal);
|
|
3921
|
+
}
|
|
3922
|
+
signal.active += 1;
|
|
3923
|
+
if (attempt > 1 || status === "failed") {
|
|
3924
|
+
signal.retrying += 1;
|
|
3925
|
+
}
|
|
3926
|
+
if (latencySampleMs > 0) {
|
|
3927
|
+
signal.mergeLatencyMsTotal += latencySampleMs;
|
|
3928
|
+
signal.mergeLatencySamples += 1;
|
|
3929
|
+
signal.maxMergeLatencyMs = Math.max(
|
|
3930
|
+
signal.maxMergeLatencyMs,
|
|
3931
|
+
latencySampleMs,
|
|
3932
|
+
);
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
return signals;
|
|
3937
|
+
}
|
|
3938
|
+
|
|
3939
|
+
_computeRepoAreaEffectiveLimit(area, activeSignals = null) {
|
|
3940
|
+
const configuredLimit = Number(this.repoAreaParallelLimit || 0);
|
|
3941
|
+
if (!Number.isFinite(configuredLimit) || configuredLimit <= 0) {
|
|
3942
|
+
return 0;
|
|
3943
|
+
}
|
|
3944
|
+
if (configuredLimit <= 1) return 1;
|
|
3945
|
+
|
|
3946
|
+
const signal = activeSignals?.get(area) || {
|
|
3947
|
+
active: 0,
|
|
3948
|
+
retrying: 0,
|
|
3949
|
+
mergeLatencyMsTotal: 0,
|
|
3950
|
+
mergeLatencySamples: 0,
|
|
3951
|
+
};
|
|
3952
|
+
const failureRate =
|
|
3953
|
+
signal.active > 0 ? signal.retrying / signal.active : 0;
|
|
3954
|
+
const averageMergeLatencyMs =
|
|
3955
|
+
signal.mergeLatencySamples > 0
|
|
3956
|
+
? signal.mergeLatencyMsTotal / signal.mergeLatencySamples
|
|
3957
|
+
: 0;
|
|
3958
|
+
|
|
3959
|
+
let penalty = 0;
|
|
3960
|
+
if (signal.active > 0 && failureRate >= 0.5) {
|
|
3961
|
+
penalty += 1;
|
|
3962
|
+
}
|
|
3963
|
+
if (
|
|
3964
|
+
signal.active > 0 &&
|
|
3965
|
+
averageMergeLatencyMs >= REPO_AREA_SLOW_MERGE_LATENCY_MS
|
|
3966
|
+
) {
|
|
3967
|
+
penalty += 1;
|
|
3968
|
+
}
|
|
3969
|
+
if (
|
|
3970
|
+
signal.active > 1 &&
|
|
3971
|
+
failureRate >= 0.75 &&
|
|
3972
|
+
averageMergeLatencyMs >= REPO_AREA_VERY_SLOW_MERGE_LATENCY_MS
|
|
3973
|
+
) {
|
|
3974
|
+
penalty += 1;
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3977
|
+
return Math.max(
|
|
3978
|
+
1,
|
|
3979
|
+
configuredLimit - Math.min(configuredLimit - 1, penalty),
|
|
3980
|
+
);
|
|
3981
|
+
}
|
|
3982
|
+
|
|
3983
|
+
_buildRepoAreaLockStatus(now = Date.now()) {
|
|
3984
|
+
const activeCounts = this._buildActiveRepoAreaCounts();
|
|
3985
|
+
const activeSignals = this._buildActiveRepoAreaSignals(now);
|
|
3986
|
+
const waitingCounts = new Map();
|
|
3987
|
+
for (const pending of this._repoAreaPendingWaits.values()) {
|
|
3988
|
+
waitingCounts.set(
|
|
3989
|
+
pending.area,
|
|
3990
|
+
(waitingCounts.get(pending.area) || 0) + 1,
|
|
3991
|
+
);
|
|
3992
|
+
}
|
|
3993
|
+
|
|
3994
|
+
const areas = new Set([
|
|
3995
|
+
...activeCounts.keys(),
|
|
3996
|
+
...activeSignals.keys(),
|
|
3997
|
+
...waitingCounts.keys(),
|
|
3998
|
+
...this._repoAreaLockMetrics.keys(),
|
|
3999
|
+
]);
|
|
4000
|
+
|
|
4001
|
+
const items = Array.from(areas)
|
|
4002
|
+
.sort()
|
|
4003
|
+
.map((area) => {
|
|
4004
|
+
const metric = this._getRepoAreaLockMetric(area) || {
|
|
4005
|
+
conflicts: 0,
|
|
4006
|
+
blockedDispatches: 0,
|
|
4007
|
+
selectedDispatches: 0,
|
|
4008
|
+
waitMsTotal: 0,
|
|
4009
|
+
waitSamples: 0,
|
|
4010
|
+
maxWaitMs: 0,
|
|
4011
|
+
lastConflictAt: null,
|
|
4012
|
+
lastSelectedAt: null,
|
|
4013
|
+
};
|
|
4014
|
+
const signal = activeSignals.get(area) || {
|
|
4015
|
+
active: 0,
|
|
4016
|
+
retrying: 0,
|
|
4017
|
+
mergeLatencyMsTotal: 0,
|
|
4018
|
+
mergeLatencySamples: 0,
|
|
4019
|
+
maxMergeLatencyMs: 0,
|
|
4020
|
+
};
|
|
4021
|
+
const active = activeCounts.get(area) || 0;
|
|
4022
|
+
const waitingTasks = waitingCounts.get(area) || 0;
|
|
4023
|
+
const activeFailureRate =
|
|
4024
|
+
signal.active > 0 ? signal.retrying / signal.active : 0;
|
|
4025
|
+
const averageMergeLatencyMs =
|
|
4026
|
+
signal.mergeLatencySamples > 0
|
|
4027
|
+
? signal.mergeLatencyMsTotal / signal.mergeLatencySamples
|
|
4028
|
+
: 0;
|
|
4029
|
+
|
|
4030
|
+
return {
|
|
4031
|
+
area,
|
|
4032
|
+
configuredLimit: Number(this.repoAreaParallelLimit || 0),
|
|
4033
|
+
effectiveLimit: this._computeRepoAreaEffectiveLimit(area, activeSignals),
|
|
4034
|
+
activeSlots: active,
|
|
4035
|
+
waitingTasks,
|
|
4036
|
+
activeFailureRate,
|
|
4037
|
+
averageMergeLatencyMs,
|
|
4038
|
+
maxMergeLatencyMs: signal.maxMergeLatencyMs || 0,
|
|
4039
|
+
conflicts: metric.conflicts,
|
|
4040
|
+
blockedDispatches: metric.blockedDispatches,
|
|
4041
|
+
selectedDispatches: metric.selectedDispatches,
|
|
4042
|
+
averageWaitMs:
|
|
4043
|
+
metric.waitSamples > 0 ? metric.waitMsTotal / metric.waitSamples : 0,
|
|
4044
|
+
maxWaitMs: metric.maxWaitMs,
|
|
4045
|
+
waitSamples: metric.waitSamples,
|
|
4046
|
+
lastConflictAt: metric.lastConflictAt,
|
|
4047
|
+
lastSelectedAt: metric.lastSelectedAt,
|
|
4048
|
+
};
|
|
4049
|
+
});
|
|
4050
|
+
|
|
4051
|
+
return {
|
|
4052
|
+
enabled: Number(this.repoAreaParallelLimit || 0) > 0,
|
|
4053
|
+
configuredLimit: Number(this.repoAreaParallelLimit || 0),
|
|
4054
|
+
areas: items,
|
|
4055
|
+
totals: {
|
|
4056
|
+
conflicts: items.reduce((sum, item) => sum + item.conflicts, 0),
|
|
4057
|
+
blockedDispatches: items.reduce(
|
|
4058
|
+
(sum, item) => sum + item.blockedDispatches,
|
|
4059
|
+
0,
|
|
4060
|
+
),
|
|
4061
|
+
waitingTasks: items.reduce((sum, item) => sum + item.waitingTasks, 0),
|
|
4062
|
+
},
|
|
4063
|
+
lastDispatch: {
|
|
4064
|
+
...this._repoAreaDispatchCycle,
|
|
4065
|
+
blockedByArea: { ...this._repoAreaDispatchCycle.blockedByArea },
|
|
4066
|
+
saturatedAreas: [...this._repoAreaDispatchCycle.saturatedAreas],
|
|
4067
|
+
},
|
|
4068
|
+
};
|
|
4069
|
+
}
|
|
3431
4070
|
_selectTasksForBaseBranchLimit(candidates, remaining) {
|
|
3432
4071
|
if (!Array.isArray(candidates) || candidates.length === 0) return [];
|
|
3433
4072
|
if (!Number.isFinite(remaining) || remaining <= 0) return [];
|
|
@@ -3440,12 +4079,29 @@ class TaskExecutor {
|
|
|
3440
4079
|
return candidates.slice(0, remaining);
|
|
3441
4080
|
}
|
|
3442
4081
|
|
|
4082
|
+
const now = Date.now();
|
|
4083
|
+
this._pruneRepoAreaPendingWaits(candidates, now);
|
|
4084
|
+
|
|
3443
4085
|
const baseBranchCounts = enforceBaseBranch
|
|
3444
4086
|
? this._buildActiveBaseBranchCounts()
|
|
3445
4087
|
: new Map();
|
|
3446
4088
|
const repoAreaCounts = enforceRepoArea
|
|
3447
4089
|
? this._buildActiveRepoAreaCounts()
|
|
3448
4090
|
: new Map();
|
|
4091
|
+
const activeRepoAreaSignals = enforceRepoArea
|
|
4092
|
+
? this._buildActiveRepoAreaSignals(now)
|
|
4093
|
+
: new Map();
|
|
4094
|
+
const effectiveRepoAreaLimits = new Map();
|
|
4095
|
+
this._repoAreaDispatchCycle = {
|
|
4096
|
+
cycle: Number(this._repoAreaDispatchCycle?.cycle || 0) + 1,
|
|
4097
|
+
at: new Date(now).toISOString(),
|
|
4098
|
+
candidateCount: candidates.length,
|
|
4099
|
+
remaining,
|
|
4100
|
+
selectedCount: 0,
|
|
4101
|
+
blockedTasks: 0,
|
|
4102
|
+
blockedByArea: {},
|
|
4103
|
+
saturatedAreas: [],
|
|
4104
|
+
};
|
|
3449
4105
|
|
|
3450
4106
|
const selected = [];
|
|
3451
4107
|
for (const task of candidates) {
|
|
@@ -3462,10 +4118,17 @@ class TaskExecutor {
|
|
|
3462
4118
|
|
|
3463
4119
|
if (enforceRepoArea) {
|
|
3464
4120
|
const areas = this._extractTaskRepoAreas(task);
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
4121
|
+
const blockedAreas = areas.filter((area) => {
|
|
4122
|
+
if (!effectiveRepoAreaLimits.has(area)) {
|
|
4123
|
+
effectiveRepoAreaLimits.set(
|
|
4124
|
+
area,
|
|
4125
|
+
this._computeRepoAreaEffectiveLimit(area, activeRepoAreaSignals),
|
|
4126
|
+
);
|
|
4127
|
+
}
|
|
4128
|
+
const effectiveLimit = effectiveRepoAreaLimits.get(area) || repoAreaLimit;
|
|
4129
|
+
return (repoAreaCounts.get(area) || 0) >= effectiveLimit;
|
|
4130
|
+
});
|
|
4131
|
+
if (blockedAreas.length > 0) {
|
|
3469
4132
|
if (enforceBaseBranch) {
|
|
3470
4133
|
const key = this._resolveTaskBaseBranchKey(task);
|
|
3471
4134
|
if (key) {
|
|
@@ -3473,16 +4136,39 @@ class TaskExecutor {
|
|
|
3473
4136
|
baseBranchCounts.set(key, Math.max(0, current - 1));
|
|
3474
4137
|
}
|
|
3475
4138
|
}
|
|
4139
|
+
this._repoAreaDispatchCycle.blockedTasks += 1;
|
|
4140
|
+
for (const area of blockedAreas) {
|
|
4141
|
+
const metric = this._getRepoAreaLockMetric(area);
|
|
4142
|
+
if (metric) {
|
|
4143
|
+
metric.conflicts += 1;
|
|
4144
|
+
metric.blockedDispatches += 1;
|
|
4145
|
+
metric.lastConflictAt = this._repoAreaDispatchCycle.at;
|
|
4146
|
+
}
|
|
4147
|
+
this._beginRepoAreaWait(task?.id || task?.task_id, area, now);
|
|
4148
|
+
this._repoAreaDispatchCycle.blockedByArea[area] =
|
|
4149
|
+
(this._repoAreaDispatchCycle.blockedByArea[area] || 0) + 1;
|
|
4150
|
+
}
|
|
3476
4151
|
continue;
|
|
3477
4152
|
}
|
|
4153
|
+
this._clearRepoAreaWaitsForTask(task, now);
|
|
3478
4154
|
for (const area of areas) {
|
|
3479
4155
|
repoAreaCounts.set(area, (repoAreaCounts.get(area) || 0) + 1);
|
|
4156
|
+
const metric = this._getRepoAreaLockMetric(area);
|
|
4157
|
+
if (metric) {
|
|
4158
|
+
metric.selectedDispatches += 1;
|
|
4159
|
+
metric.lastSelectedAt = this._repoAreaDispatchCycle.at;
|
|
4160
|
+
}
|
|
3480
4161
|
}
|
|
3481
4162
|
}
|
|
3482
4163
|
|
|
3483
4164
|
selected.push(task);
|
|
3484
4165
|
}
|
|
3485
4166
|
|
|
4167
|
+
this._repoAreaDispatchCycle.selectedCount = selected.length;
|
|
4168
|
+
this._repoAreaDispatchCycle.saturatedAreas = Object.keys(
|
|
4169
|
+
this._repoAreaDispatchCycle.blockedByArea,
|
|
4170
|
+
).sort();
|
|
4171
|
+
|
|
3486
4172
|
return selected;
|
|
3487
4173
|
}
|
|
3488
4174
|
_isBaseBranchLimitReached(task) {
|
|
@@ -4118,5 +4804,3 @@ export function isExecutorDisabled() {
|
|
|
4118
4804
|
|
|
4119
4805
|
export { TaskExecutor };
|
|
4120
4806
|
export default TaskExecutor;
|
|
4121
|
-
|
|
4122
|
-
|