@zq-silk/yui 0.6.4 → 0.6.6
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/README.md +11 -3
- package/dist/cli/updateOrchestrator.js +12 -6
- package/dist/cli.js +12 -0
- package/dist/controller/agentRuntimeObserver.js +76 -16
- package/dist/controller/controller.js +398 -76
- package/dist/controller/fileSchedulerStoreAdapter.js +60 -51
- package/dist/controller/jobSupervisor.js +2 -16
- package/dist/controller/resourceInventoryLinux.js +73 -20
- package/dist/controller/runtime.js +1 -0
- package/dist/controller/runtimeEventProcessor.js +171 -31
- package/dist/coordination/keyedWorkQueue.js +189 -0
- package/dist/runtime/runtimeSessionCandidate.js +43 -0
- package/dist/scheduler/activeRoleRunDelivery.js +2 -4
- package/dist/scheduler/activeTaskProgress.js +2 -4
- package/dist/scheduler/leaderWakeupProcessor.js +3 -1
- package/dist/scheduler/ports.js +35 -2
- package/dist/scheduler/roleRunLiveness.js +44 -22
- package/dist/scheduler/roleRunStall.js +49 -25
- package/dist/storage/sqliteSchema.js +363 -14
- package/dist/storage/sqliteStore.js +403 -30
- package/dist/storage/storeRpc.js +5 -0
- package/dist/storage/taskStore.js +104 -6
- package/i18n/README.zh-CN.md +7 -1
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@ export class FileRuntimeEventProcessor {
|
|
|
6
6
|
observer;
|
|
7
7
|
options;
|
|
8
8
|
drivers;
|
|
9
|
+
drainLaneCursor = 0;
|
|
9
10
|
constructor(inbox, observer, options = {}) {
|
|
10
11
|
this.inbox = inbox;
|
|
11
12
|
this.observer = observer;
|
|
@@ -25,39 +26,62 @@ export class FileRuntimeEventProcessor {
|
|
|
25
26
|
}
|
|
26
27
|
const coalesced = coalesceRuntimeProgress(events);
|
|
27
28
|
const maximum = positiveInteger(this.options.maxEventsPerDrain, DEFAULT_MAX_RUNTIME_EVENTS_PER_DRAIN);
|
|
28
|
-
const
|
|
29
|
+
const selection = selectDrainBatch(coalesced, maximum, this.drainLaneCursor);
|
|
30
|
+
this.drainLaneCursor = selection.nextLaneCursor;
|
|
31
|
+
const selected = selection.candidates;
|
|
32
|
+
const failedTaskIds = new Set();
|
|
29
33
|
let stateTransactions = 0;
|
|
30
34
|
if (selected.length > 0 && this.observer.withRuntimeEventTransaction !== undefined) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
this.finalizeOne(folded, acknowledgedEventIds, deferred, failed);
|
|
35
|
+
let offset = 0;
|
|
36
|
+
while (offset < selected.length) {
|
|
37
|
+
const wave = selectTaskOrderedWave(selected, offset, failedTaskIds);
|
|
38
|
+
offset = wave.nextOffset;
|
|
39
|
+
if (wave.candidates.length === 0)
|
|
40
|
+
continue;
|
|
41
|
+
try {
|
|
42
|
+
stateTransactions += 1;
|
|
43
|
+
const folded = this.observer.withRuntimeEventTransaction(() => (wave.candidates.map((candidate) => this.foldOne(candidate, now))));
|
|
44
|
+
for (const result of folded) {
|
|
45
|
+
const failure = this.finalizeOne(result, acknowledgedEventIds, deferred);
|
|
46
|
+
if (failure !== undefined) {
|
|
47
|
+
recordDrainFailure(failure, failed, failedTaskIds);
|
|
48
|
+
}
|
|
46
49
|
}
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// A failed aggregate transaction commits nothing. Retry each candidate
|
|
53
|
+
// independently so one bad event cannot strand unrelated Tasks.
|
|
54
|
+
for (const candidate of wave.candidates) {
|
|
55
|
+
if (isTaskCandidateBlocked(candidate.event, failedTaskIds))
|
|
56
|
+
continue;
|
|
57
|
+
try {
|
|
58
|
+
stateTransactions += 1;
|
|
59
|
+
const folded = this.observer.withRuntimeEventTransaction(() => (this.foldOne(candidate, now)));
|
|
60
|
+
const failure = this.finalizeOne(folded, acknowledgedEventIds, deferred);
|
|
61
|
+
if (failure !== undefined) {
|
|
62
|
+
recordDrainFailure(failure, failed, failedTaskIds);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (candidateError) {
|
|
66
|
+
recordDrainFailure(candidateDrainFailure(candidate.event, candidateError), failed, failedTaskIds);
|
|
67
|
+
}
|
|
49
68
|
}
|
|
50
69
|
}
|
|
51
70
|
}
|
|
52
71
|
}
|
|
53
72
|
else {
|
|
54
73
|
for (const candidate of selected) {
|
|
74
|
+
if (isTaskCandidateBlocked(candidate.event, failedTaskIds))
|
|
75
|
+
continue;
|
|
55
76
|
try {
|
|
56
77
|
const folded = this.foldOne(candidate, now);
|
|
57
|
-
this.finalizeOne(folded, acknowledgedEventIds, deferred
|
|
78
|
+
const failure = this.finalizeOne(folded, acknowledgedEventIds, deferred);
|
|
79
|
+
if (failure !== undefined) {
|
|
80
|
+
recordDrainFailure(failure, failed, failedTaskIds);
|
|
81
|
+
}
|
|
58
82
|
}
|
|
59
83
|
catch (error) {
|
|
60
|
-
|
|
84
|
+
recordDrainFailure(candidateDrainFailure(candidate.event, error), failed, failedTaskIds);
|
|
61
85
|
}
|
|
62
86
|
}
|
|
63
87
|
}
|
|
@@ -117,13 +141,13 @@ export class FileRuntimeEventProcessor {
|
|
|
117
141
|
}
|
|
118
142
|
return this.observer.observeRuntimeObservation?.(event.observation, now) ?? "obsolete";
|
|
119
143
|
}
|
|
120
|
-
finalizeOne(folded, acknowledged, deferred
|
|
144
|
+
finalizeOne(folded, acknowledged, deferred) {
|
|
121
145
|
const { candidate, outcome } = folded;
|
|
122
146
|
try {
|
|
123
147
|
if (outcome === "deferred") {
|
|
124
148
|
deferred.push(candidate.event);
|
|
125
149
|
this.acknowledge(candidate.representedEventIds.filter((id) => id !== candidate.event.id), acknowledged);
|
|
126
|
-
return;
|
|
150
|
+
return undefined;
|
|
127
151
|
}
|
|
128
152
|
if (folded.notifyTaskRuntime) {
|
|
129
153
|
const event = candidate.event;
|
|
@@ -158,9 +182,10 @@ export class FileRuntimeEventProcessor {
|
|
|
158
182
|
}
|
|
159
183
|
}
|
|
160
184
|
this.acknowledge(candidate.representedEventIds, acknowledged);
|
|
185
|
+
return undefined;
|
|
161
186
|
}
|
|
162
187
|
catch (error) {
|
|
163
|
-
|
|
188
|
+
return candidateDrainFailure(candidate.event, error);
|
|
164
189
|
}
|
|
165
190
|
}
|
|
166
191
|
/**
|
|
@@ -331,11 +356,62 @@ export function coalesceRuntimeProgress(events, instrumentation = {}) {
|
|
|
331
356
|
flush();
|
|
332
357
|
return result;
|
|
333
358
|
}
|
|
334
|
-
function selectDrainBatch(events, maximum) {
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
|
|
359
|
+
function selectDrainBatch(events, maximum, laneCursor = 0) {
|
|
360
|
+
// Select one candidate per Task per round. A poison prefix from one Task
|
|
361
|
+
// therefore cannot consume the whole bounded batch before a later Task gets
|
|
362
|
+
// a chance to fold. Each lane itself remains in arrival order, preserving
|
|
363
|
+
// the same-Task semantic fence; only cross-Task order is interleaved.
|
|
364
|
+
const lanes = new Map();
|
|
365
|
+
for (const candidate of events) {
|
|
366
|
+
const key = drainLaneKey(candidate.event);
|
|
367
|
+
const lane = lanes.get(key);
|
|
368
|
+
if (lane === undefined)
|
|
369
|
+
lanes.set(key, [candidate]);
|
|
370
|
+
else
|
|
371
|
+
lane.push(candidate);
|
|
372
|
+
}
|
|
373
|
+
const laneStates = [...lanes.values()].map((candidates) => ({
|
|
374
|
+
candidates,
|
|
375
|
+
offset: 0
|
|
376
|
+
}));
|
|
377
|
+
if (laneStates.length === 0) {
|
|
378
|
+
return { candidates: [], nextLaneCursor: 0 };
|
|
379
|
+
}
|
|
380
|
+
const startLane = positiveModulo(laneCursor, laneStates.length);
|
|
381
|
+
const selected = [];
|
|
382
|
+
let lastLane = startLane;
|
|
383
|
+
while (selected.length < maximum) {
|
|
384
|
+
let progressed = false;
|
|
385
|
+
for (let laneOffset = 0; laneOffset < laneStates.length; laneOffset += 1) {
|
|
386
|
+
const laneIndex = (startLane + laneOffset) % laneStates.length;
|
|
387
|
+
const lane = laneStates[laneIndex];
|
|
388
|
+
const candidate = lane.candidates[lane.offset];
|
|
389
|
+
if (candidate === undefined)
|
|
390
|
+
continue;
|
|
391
|
+
lane.offset += 1;
|
|
392
|
+
selected.push(candidate);
|
|
393
|
+
lastLane = laneIndex;
|
|
394
|
+
progressed = true;
|
|
395
|
+
if (selected.length >= maximum)
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
// Every non-empty lane advances its offset when selected. Keep this
|
|
399
|
+
// guard explicit so malformed/empty input cannot create a zero-progress
|
|
400
|
+
// drain loop.
|
|
401
|
+
if (!progressed)
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
candidates: selected,
|
|
406
|
+
nextLaneCursor: (lastLane + 1) % laneStates.length
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
function positiveModulo(value, modulus) {
|
|
410
|
+
return ((value % modulus) + modulus) % modulus;
|
|
411
|
+
}
|
|
412
|
+
function drainLaneKey(event) {
|
|
413
|
+
const taskId = candidateTaskId(event);
|
|
414
|
+
return taskId === undefined ? "global" : `task:${taskId}`;
|
|
339
415
|
}
|
|
340
416
|
function progressStreamKey(event) {
|
|
341
417
|
const { fence, payload } = event.observation;
|
|
@@ -362,7 +438,7 @@ function emptyDrainFailure(error) {
|
|
|
362
438
|
return {
|
|
363
439
|
acknowledgedEventIds: [],
|
|
364
440
|
deferred: [],
|
|
365
|
-
failed: [{ error }],
|
|
441
|
+
failed: [{ scope: "unknown", error }],
|
|
366
442
|
remainingEventCount: 0,
|
|
367
443
|
metrics: {
|
|
368
444
|
listedEventCount: 0,
|
|
@@ -388,6 +464,64 @@ function isObsoleteRuntimeTurnObservation(value) {
|
|
|
388
464
|
&& value !== null
|
|
389
465
|
&& value.disposition === "obsolete";
|
|
390
466
|
}
|
|
467
|
+
function candidateDrainFailure(event, error) {
|
|
468
|
+
const eventId = nonEmptyString(event.id);
|
|
469
|
+
if (event.scope === "task") {
|
|
470
|
+
const taskId = nonEmptyString(event.taskId);
|
|
471
|
+
if (eventId === undefined || taskId === undefined) {
|
|
472
|
+
// A Task failure must carry a durable inbox fence. If an injected port
|
|
473
|
+
// violates that parsed-event contract, do not incorrectly isolate it.
|
|
474
|
+
return {
|
|
475
|
+
...(eventId === undefined ? {} : { eventId }),
|
|
476
|
+
scope: "unknown",
|
|
477
|
+
error
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
return { eventId, scope: "task", taskId, error };
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
...(eventId === undefined ? {} : { eventId }),
|
|
484
|
+
scope: "global",
|
|
485
|
+
error
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
function selectTaskOrderedWave(candidates, offset, failedTaskIds) {
|
|
489
|
+
const selected = [];
|
|
490
|
+
const selectedTaskIds = new Set();
|
|
491
|
+
let nextOffset = offset;
|
|
492
|
+
while (nextOffset < candidates.length) {
|
|
493
|
+
const candidate = candidates[nextOffset];
|
|
494
|
+
const taskId = candidateTaskId(candidate.event);
|
|
495
|
+
if (taskId !== undefined && failedTaskIds.has(taskId)) {
|
|
496
|
+
nextOffset += 1;
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (taskId !== undefined && selectedTaskIds.has(taskId))
|
|
500
|
+
break;
|
|
501
|
+
selected.push(candidate);
|
|
502
|
+
if (taskId !== undefined)
|
|
503
|
+
selectedTaskIds.add(taskId);
|
|
504
|
+
nextOffset += 1;
|
|
505
|
+
}
|
|
506
|
+
return { candidates: selected, nextOffset };
|
|
507
|
+
}
|
|
508
|
+
function recordDrainFailure(failure, failed, failedTaskIds) {
|
|
509
|
+
failed.push(failure);
|
|
510
|
+
if (failure.scope === "task")
|
|
511
|
+
failedTaskIds.add(failure.taskId);
|
|
512
|
+
}
|
|
513
|
+
function isTaskCandidateBlocked(event, failedTaskIds) {
|
|
514
|
+
const taskId = candidateTaskId(event);
|
|
515
|
+
return taskId !== undefined && failedTaskIds.has(taskId);
|
|
516
|
+
}
|
|
517
|
+
function candidateTaskId(event) {
|
|
518
|
+
if (event.scope !== "task" || nonEmptyString(event.id) === undefined)
|
|
519
|
+
return undefined;
|
|
520
|
+
return nonEmptyString(event.taskId);
|
|
521
|
+
}
|
|
522
|
+
function nonEmptyString(value) {
|
|
523
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
524
|
+
}
|
|
391
525
|
/**
|
|
392
526
|
* Build an {@link AsyncRuntimeTurnEventObserver} that forwards every fold to the
|
|
393
527
|
* worker-hosted adapter via {@link AsyncObserverInvoker}. The adapter's fold
|
|
@@ -416,6 +550,7 @@ export class AsyncRuntimeEventProcessor {
|
|
|
416
550
|
observer;
|
|
417
551
|
options;
|
|
418
552
|
drivers;
|
|
553
|
+
drainLaneCursor = 0;
|
|
419
554
|
constructor(inbox, observer, options = {}) {
|
|
420
555
|
this.inbox = inbox;
|
|
421
556
|
this.observer = observer;
|
|
@@ -435,9 +570,14 @@ export class AsyncRuntimeEventProcessor {
|
|
|
435
570
|
}
|
|
436
571
|
const coalesced = coalesceRuntimeProgress(events);
|
|
437
572
|
const maximum = positiveInteger(this.options.maxEventsPerDrain, DEFAULT_MAX_RUNTIME_EVENTS_PER_DRAIN);
|
|
438
|
-
const
|
|
573
|
+
const selection = selectDrainBatch(coalesced, maximum, this.drainLaneCursor);
|
|
574
|
+
this.drainLaneCursor = selection.nextLaneCursor;
|
|
575
|
+
const selected = selection.candidates;
|
|
576
|
+
const failedTaskIds = new Set();
|
|
439
577
|
for (const candidate of selected) {
|
|
440
578
|
const event = candidate.event;
|
|
579
|
+
if (isTaskCandidateBlocked(event, failedTaskIds))
|
|
580
|
+
continue;
|
|
441
581
|
try {
|
|
442
582
|
let outcome = "applied";
|
|
443
583
|
if (event.type === "native-turn-completed") {
|
|
@@ -460,7 +600,7 @@ export class AsyncRuntimeEventProcessor {
|
|
|
460
600
|
this.acknowledge(candidate.representedEventIds, acknowledgedEventIds);
|
|
461
601
|
}
|
|
462
602
|
catch (error) {
|
|
463
|
-
|
|
603
|
+
recordDrainFailure(candidateDrainFailure(event, error), failed, failedTaskIds);
|
|
464
604
|
}
|
|
465
605
|
}
|
|
466
606
|
const acknowledged = new Set(acknowledgedEventIds);
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A FIFO work queue that coalesces signals by key.
|
|
3
|
+
*
|
|
4
|
+
* At most one item for a key can be processing. Signals received while that
|
|
5
|
+
* key is processing mark it dirty, causing exactly one replay at the FIFO tail
|
|
6
|
+
* after the current item is done. Different keys can be leased concurrently by
|
|
7
|
+
* multiple consumers.
|
|
8
|
+
*
|
|
9
|
+
* shutdown() is graceful: it stops accepting signals but preserves all work
|
|
10
|
+
* accepted before shutdown, including dirty replays. abortPending() instead
|
|
11
|
+
* drops queued work and dirty replays while allowing leased items to finish.
|
|
12
|
+
* Consumers must complete every leased item. drain() only waits for an idle
|
|
13
|
+
* queue; it does not close the queue or process work itself.
|
|
14
|
+
*/
|
|
15
|
+
export class KeyedWorkQueue {
|
|
16
|
+
#states = new Map();
|
|
17
|
+
#ready = [];
|
|
18
|
+
#takeWaiters = [];
|
|
19
|
+
#drainWaiters = [];
|
|
20
|
+
#readyHead = 0;
|
|
21
|
+
#takeWaiterHead = 0;
|
|
22
|
+
#closed = false;
|
|
23
|
+
#aborted = false;
|
|
24
|
+
#closeCompletion;
|
|
25
|
+
/**
|
|
26
|
+
* Adds or coalesces a key. Returns false after either stop mode has started.
|
|
27
|
+
*/
|
|
28
|
+
signal(key) {
|
|
29
|
+
if (this.#closed)
|
|
30
|
+
return false;
|
|
31
|
+
const state = this.#states.get(key);
|
|
32
|
+
if (state === undefined) {
|
|
33
|
+
this.#states.set(key, { phase: "queued" });
|
|
34
|
+
this.#ready.push(key);
|
|
35
|
+
this.#dispatchReadyWork();
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (state.phase === "processing")
|
|
39
|
+
state.dirty = true;
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Leases the next ready key. It returns undefined after graceful shutdown
|
|
44
|
+
* has drained, or immediately after pending work has been aborted.
|
|
45
|
+
*/
|
|
46
|
+
take() {
|
|
47
|
+
if (this.#aborted)
|
|
48
|
+
return Promise.resolve(undefined);
|
|
49
|
+
if (this.#hasReadyWork()) {
|
|
50
|
+
return Promise.resolve(this.#leaseReadyWork());
|
|
51
|
+
}
|
|
52
|
+
if (this.#closed && this.#states.size === 0) {
|
|
53
|
+
return Promise.resolve(undefined);
|
|
54
|
+
}
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
this.#takeWaiters.push(resolve);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolves the next time no key is queued or processing. Later signals may
|
|
61
|
+
* make the queue non-idle again unless shutdown() has already started.
|
|
62
|
+
*/
|
|
63
|
+
drain() {
|
|
64
|
+
if (this.#states.size === 0)
|
|
65
|
+
return Promise.resolve();
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
this.#drainWaiters.push(resolve);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Stops accepting signals and resolves after all previously accepted work
|
|
72
|
+
* has been completed. Calling shutdown repeatedly returns the same promise.
|
|
73
|
+
*/
|
|
74
|
+
shutdown() {
|
|
75
|
+
const stopped = this.#beginClose();
|
|
76
|
+
this.#settleIdleWaiters();
|
|
77
|
+
return stopped;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Stops accepting signals, drops queued keys and dirty replays, and resolves
|
|
81
|
+
* after already leased items are completed. Pending and later take() calls
|
|
82
|
+
* receive undefined. Repeated calls are idempotent.
|
|
83
|
+
*/
|
|
84
|
+
abortPending() {
|
|
85
|
+
const stopped = this.#beginClose();
|
|
86
|
+
if (this.#aborted)
|
|
87
|
+
return stopped;
|
|
88
|
+
this.#aborted = true;
|
|
89
|
+
for (const [key, state] of this.#states) {
|
|
90
|
+
if (state.phase === "queued") {
|
|
91
|
+
this.#states.delete(key);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
state.dirty = false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
this.#ready.length = 0;
|
|
98
|
+
this.#readyHead = 0;
|
|
99
|
+
this.#resolveTakeWaiters();
|
|
100
|
+
this.#settleIdleWaiters();
|
|
101
|
+
return stopped;
|
|
102
|
+
}
|
|
103
|
+
#hasReadyWork() {
|
|
104
|
+
return this.#readyHead < this.#ready.length;
|
|
105
|
+
}
|
|
106
|
+
#hasTakeWaiter() {
|
|
107
|
+
return this.#takeWaiterHead < this.#takeWaiters.length;
|
|
108
|
+
}
|
|
109
|
+
#leaseReadyWork() {
|
|
110
|
+
const key = this.#ready[this.#readyHead++];
|
|
111
|
+
if (!this.#hasReadyWork()) {
|
|
112
|
+
this.#ready.length = 0;
|
|
113
|
+
this.#readyHead = 0;
|
|
114
|
+
}
|
|
115
|
+
const state = this.#states.get(key);
|
|
116
|
+
if (state?.phase !== "queued") {
|
|
117
|
+
throw new Error("Keyed work queue invariant failed: ready key was not queued.");
|
|
118
|
+
}
|
|
119
|
+
const token = Symbol("keyed-work-item");
|
|
120
|
+
this.#states.set(key, { phase: "processing", dirty: false, token });
|
|
121
|
+
let completed = false;
|
|
122
|
+
return {
|
|
123
|
+
key,
|
|
124
|
+
done: () => {
|
|
125
|
+
if (completed)
|
|
126
|
+
throw new Error("Keyed work item was already completed.");
|
|
127
|
+
completed = true;
|
|
128
|
+
this.#complete(key, token);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
#complete(key, token) {
|
|
133
|
+
const state = this.#states.get(key);
|
|
134
|
+
if (state?.phase !== "processing" || state.token !== token) {
|
|
135
|
+
throw new Error("Keyed work queue invariant failed: completed lease is not active.");
|
|
136
|
+
}
|
|
137
|
+
if (state.dirty && !this.#aborted) {
|
|
138
|
+
this.#states.set(key, { phase: "queued" });
|
|
139
|
+
this.#ready.push(key);
|
|
140
|
+
this.#dispatchReadyWork();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
this.#states.delete(key);
|
|
144
|
+
this.#settleIdleWaiters();
|
|
145
|
+
}
|
|
146
|
+
#dispatchReadyWork() {
|
|
147
|
+
while (this.#hasReadyWork() && this.#hasTakeWaiter()) {
|
|
148
|
+
const resolve = this.#takeWaiters[this.#takeWaiterHead++];
|
|
149
|
+
const item = this.#leaseReadyWork();
|
|
150
|
+
resolve(item);
|
|
151
|
+
}
|
|
152
|
+
if (!this.#hasTakeWaiter()) {
|
|
153
|
+
this.#takeWaiters.length = 0;
|
|
154
|
+
this.#takeWaiterHead = 0;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
#settleIdleWaiters() {
|
|
158
|
+
if (this.#states.size !== 0)
|
|
159
|
+
return;
|
|
160
|
+
const drains = this.#drainWaiters.splice(0);
|
|
161
|
+
for (const resolve of drains)
|
|
162
|
+
resolve();
|
|
163
|
+
if (!this.#closed)
|
|
164
|
+
return;
|
|
165
|
+
this.#resolveTakeWaiters();
|
|
166
|
+
this.#closeCompletion?.resolve();
|
|
167
|
+
}
|
|
168
|
+
#beginClose() {
|
|
169
|
+
if (this.#closeCompletion === undefined) {
|
|
170
|
+
this.#closeCompletion = completion();
|
|
171
|
+
this.#closed = true;
|
|
172
|
+
}
|
|
173
|
+
return this.#closeCompletion.promise;
|
|
174
|
+
}
|
|
175
|
+
#resolveTakeWaiters() {
|
|
176
|
+
while (this.#hasTakeWaiter()) {
|
|
177
|
+
this.#takeWaiters[this.#takeWaiterHead++](undefined);
|
|
178
|
+
}
|
|
179
|
+
this.#takeWaiters.length = 0;
|
|
180
|
+
this.#takeWaiterHead = 0;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function completion() {
|
|
184
|
+
let resolve;
|
|
185
|
+
const promise = new Promise((done) => {
|
|
186
|
+
resolve = done;
|
|
187
|
+
});
|
|
188
|
+
return { promise, resolve };
|
|
189
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { activeRoleAgentSession } from "../executor/agentExecutor.js";
|
|
2
|
+
/** Exact completed-Task cleanup contract shared by projection writers/readers. */
|
|
3
|
+
export function runtimeSessionRequiresCleanup(input) {
|
|
4
|
+
if (input.status === "stopped" || input.status === "broken")
|
|
5
|
+
return false;
|
|
6
|
+
return input.status === "running" || input.launchId !== undefined;
|
|
7
|
+
}
|
|
8
|
+
/** Projects only the current active Agent Session; stopped history disappears. */
|
|
9
|
+
export function projectRuntimeSessionCandidate(sessions) {
|
|
10
|
+
const active = activeRoleAgentSession(sessions);
|
|
11
|
+
if (active === null || active.status === "stopped")
|
|
12
|
+
return null;
|
|
13
|
+
return {
|
|
14
|
+
owner: sessions.owner.scope === "task"
|
|
15
|
+
? {
|
|
16
|
+
scope: "task",
|
|
17
|
+
taskId: sessions.owner.taskId,
|
|
18
|
+
roleName: sessions.owner.roleName
|
|
19
|
+
}
|
|
20
|
+
: { scope: "global", roleName: sessions.owner.roleName },
|
|
21
|
+
agentId: active.agentId,
|
|
22
|
+
adapterId: active.adapterId,
|
|
23
|
+
nativeSessionId: active.nativeSessionId,
|
|
24
|
+
...(active.launchId === undefined ? {} : { launchId: active.launchId }),
|
|
25
|
+
status: active.status,
|
|
26
|
+
sessionUpdatedAt: active.updatedAt,
|
|
27
|
+
cleanupRequired: runtimeSessionRequiresCleanup(active)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Deterministic owner order shared by all storage backends. */
|
|
31
|
+
export function compareRuntimeSessionCandidates(left, right) {
|
|
32
|
+
if (left.owner.scope !== right.owner.scope) {
|
|
33
|
+
// Preserve the historical adapter contract: Task owners precede the
|
|
34
|
+
// bounded global Role set.
|
|
35
|
+
return left.owner.scope === "task" ? -1 : 1;
|
|
36
|
+
}
|
|
37
|
+
if (left.owner.scope === "task" && right.owner.scope === "task") {
|
|
38
|
+
const task = left.owner.taskId.localeCompare(right.owner.taskId, undefined, { numeric: true });
|
|
39
|
+
if (task !== 0)
|
|
40
|
+
return task;
|
|
41
|
+
}
|
|
42
|
+
return left.owner.roleName.localeCompare(right.owner.roleName, undefined, { numeric: true });
|
|
43
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { selectedSchedulerRoles,
|
|
1
|
+
import { selectedSchedulerRoles, selectedActiveSchedulerTasks } from "./ports.js";
|
|
2
2
|
import { isSchedulerTaskWorkspaceReady } from "./ports.js";
|
|
3
3
|
import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
|
|
4
4
|
import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskMain } from "../executor/effectiveLaunch.js";
|
|
@@ -11,9 +11,7 @@ import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
|
|
|
11
11
|
*/
|
|
12
12
|
export async function processActiveRoleRunDeliveries(store, delivery, now, selection) {
|
|
13
13
|
const results = [];
|
|
14
|
-
for (const task of
|
|
15
|
-
if (task.status !== "active")
|
|
16
|
-
continue;
|
|
14
|
+
for (const task of selectedActiveSchedulerTasks(store, selection)) {
|
|
17
15
|
for (const role of selectedSchedulerRoles(store, task.id, selection)) {
|
|
18
16
|
const run = store.getActiveAgentRun(task.id, role.name);
|
|
19
17
|
// A crash after a Leader wake is durably claimed but before tmux input
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { selectedActiveSchedulerTasks, isSchedulerTaskWorkspaceReady } from "./ports.js";
|
|
2
2
|
import { queueLeaderWakeup } from "./wakeupQueue.js";
|
|
3
3
|
import { projectTaskExecution } from "./taskExecutionProjection.js";
|
|
4
4
|
import { collectTaskActionability, computeActionabilityDigest, decideOrphanWake } from "./actionability.js";
|
|
@@ -17,9 +17,7 @@ import { collectTaskActionability, computeActionabilityDigest, decideOrphanWake
|
|
|
17
17
|
*/
|
|
18
18
|
export function repairOrphanedActiveTasks(store, now, selection) {
|
|
19
19
|
const repaired = [];
|
|
20
|
-
for (const task of
|
|
21
|
-
if (task.status !== "active")
|
|
22
|
-
continue;
|
|
20
|
+
for (const task of selectedActiveSchedulerTasks(store, selection)) {
|
|
23
21
|
const roles = store.listRoles(task.id);
|
|
24
22
|
const activeRuns = roles.flatMap((role) => {
|
|
25
23
|
const run = store.getActiveAgentRun(task.id, role.name);
|
|
@@ -11,8 +11,10 @@ import { RuntimeLaunchError } from "../runtime/ports.js";
|
|
|
11
11
|
export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
12
12
|
const results = [];
|
|
13
13
|
const wakeups = selection === undefined || selection.full
|
|
14
|
-
? store.listPendingWakeups()
|
|
14
|
+
? store.listPendingWakeups().filter((wakeup) => (!selection?.blockedTaskIds?.has(wakeup.taskId)))
|
|
15
15
|
: [...selection.taskIds].flatMap((taskId) => {
|
|
16
|
+
if (selection.blockedTaskIds?.has(taskId))
|
|
17
|
+
return [];
|
|
16
18
|
const wakeup = store.getPendingWakeup(taskId);
|
|
17
19
|
return wakeup === null ? [] : [wakeup];
|
|
18
20
|
});
|
package/dist/scheduler/ports.js
CHANGED
|
@@ -4,15 +4,48 @@ export function isSchedulerTaskWorkspaceReady(task, workspace) {
|
|
|
4
4
|
}
|
|
5
5
|
/** Resolves Tasks without a global scan for a dirty reconciliation pass. */
|
|
6
6
|
export function selectedSchedulerTasks(store, selection) {
|
|
7
|
-
if (selection === undefined || selection.full)
|
|
8
|
-
return [...store.listTasks()];
|
|
7
|
+
if (selection === undefined || selection.full) {
|
|
8
|
+
return [...store.listTasks()].filter((task) => (!selection?.blockedTaskIds?.has(task.id)));
|
|
9
|
+
}
|
|
9
10
|
return [...selection.taskIds].flatMap((taskId) => {
|
|
11
|
+
if (selection.blockedTaskIds?.has(taskId))
|
|
12
|
+
return [];
|
|
10
13
|
const task = store.getTask(taskId);
|
|
11
14
|
return task === null ? [] : [task];
|
|
12
15
|
});
|
|
13
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolves only active Tasks for Controller execution phases. Full passes use
|
|
19
|
+
* the durable active index, so terminal history never enters Role, delivery,
|
|
20
|
+
* workspace, or liveness projections. Dirty passes keep their exact-key
|
|
21
|
+
* semantics and simply discard a Task that is no longer active.
|
|
22
|
+
*/
|
|
23
|
+
export function selectedActiveSchedulerTasks(store, selection) {
|
|
24
|
+
if (selection === undefined || selection.full) {
|
|
25
|
+
const indexedTaskIds = store.listActiveTaskIds?.();
|
|
26
|
+
if (indexedTaskIds === undefined) {
|
|
27
|
+
return store.listTasks().filter((task) => (task.status === "active"
|
|
28
|
+
&& !selection?.blockedTaskIds?.has(task.id)));
|
|
29
|
+
}
|
|
30
|
+
return [...indexedTaskIds].flatMap((taskId) => {
|
|
31
|
+
if (selection?.blockedTaskIds?.has(taskId))
|
|
32
|
+
return [];
|
|
33
|
+
const task = store.getTask(taskId);
|
|
34
|
+
return task?.status === "active" ? [task] : [];
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
const taskIds = selection.taskIds;
|
|
38
|
+
return [...taskIds].flatMap((taskId) => {
|
|
39
|
+
if (selection.blockedTaskIds?.has(taskId))
|
|
40
|
+
return [];
|
|
41
|
+
const task = store.getTask(taskId);
|
|
42
|
+
return task?.status === "active" ? [task] : [];
|
|
43
|
+
});
|
|
44
|
+
}
|
|
14
45
|
/** Resolves either every Role in a selected Task or only explicit Role keys. */
|
|
15
46
|
export function selectedSchedulerRoles(store, taskId, selection) {
|
|
47
|
+
if (selection?.blockedTaskIds?.has(taskId))
|
|
48
|
+
return [];
|
|
16
49
|
if (selection === undefined
|
|
17
50
|
|| selection.full
|
|
18
51
|
|| selection.allRoleTaskIds.has(taskId)) {
|