@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { selectedSchedulerRoles,
|
|
1
|
+
import { selectedSchedulerRoles, selectedActiveSchedulerTasks } from "./ports.js";
|
|
2
2
|
import { formatTaskRecordReference } from "../task/taskRecordReference.js";
|
|
3
3
|
import { queueLeaderWakeup } from "./wakeupQueue.js";
|
|
4
4
|
import { currentRoleRunProgressAt, DEFAULT_WORKFLOW_STALL_CANDIDATE_AGE_MS } from "./roleRunStall.js";
|
|
@@ -7,9 +7,9 @@ export const EXITED_ROLE_RUN_SUMMARY = "The role's tmux session exited before th
|
|
|
7
7
|
* Lightweight liveness only: an active AgentRun whose tmux role is absent is
|
|
8
8
|
* failed, then the Leader is durably queued. No TTL, cooldown, or schedules.
|
|
9
9
|
*/
|
|
10
|
-
export async function reconcileExitedRoleRuns(store, delivery, now, selection, excludedRunRefs = new Set(), liveStatuses, resourceEvidence) {
|
|
10
|
+
export async function reconcileExitedRoleRuns(store, delivery, now, selection, excludedRunRefs = new Set(), liveStatuses, resourceEvidence, targetedInventory = selection !== undefined && !selection.full) {
|
|
11
11
|
const failed = [];
|
|
12
|
-
const candidates =
|
|
12
|
+
const candidates = selectedActiveSchedulerTasks(store, selection).flatMap((task) => (selectedSchedulerRoles(store, task.id, selection).flatMap((role) => {
|
|
13
13
|
const run = store.getActiveAgentRun(task.id, role.name);
|
|
14
14
|
if (run === null)
|
|
15
15
|
return [];
|
|
@@ -35,32 +35,39 @@ export async function reconcileExitedRoleRuns(store, delivery, now, selection, e
|
|
|
35
35
|
})));
|
|
36
36
|
if (candidates.length === 0)
|
|
37
37
|
return failed;
|
|
38
|
-
const
|
|
38
|
+
const candidateTaskIds = [...new Set(candidates.map(({ task }) => task.id))]
|
|
39
|
+
.sort((left, right) => left.localeCompare(right, undefined, { numeric: true }));
|
|
40
|
+
const completing = new Set(store.listPendingRuntimeTurnCompletions(candidateTaskIds).map((completion) => (`${completion.taskId}\0${completion.roleName}\0${completion.runId}`)));
|
|
39
41
|
const eligible = candidates.filter(({ task, role, run }) => (!excludedRunRefs.has(formatTaskRecordReference(task.id, run.id, "agentRun"))
|
|
40
42
|
&& !completing.has(`${task.id}\0${role.name}\0${run.id}`)));
|
|
41
|
-
//
|
|
42
|
-
// delivery-uncertain and completion-pending Runs.
|
|
43
|
-
//
|
|
43
|
+
// Full reconciliation builds one complete provider inventory for every
|
|
44
|
+
// active Run, including delivery-uncertain and completion-pending Runs.
|
|
45
|
+
// The stall phase reuses that snapshot so one full pass never probes the
|
|
46
|
+
// same pane twice. When targetedInventory is true, a dirty pass instead
|
|
47
|
+
// uses exact probes below; stall reconciliation intentionally does not run
|
|
48
|
+
// for that bounded selection.
|
|
44
49
|
const batchSnapshot = liveStatuses !== undefined
|
|
45
50
|
&& candidates.every(({ task, role }) => liveStatuses.has(`${task.id}\0${role.name}`))
|
|
46
51
|
? {
|
|
47
52
|
statuses: liveStatuses,
|
|
48
53
|
resources: resourceEvidence ?? new Map()
|
|
49
54
|
}
|
|
50
|
-
: await inspectRoleStatuses(delivery, candidates,
|
|
51
|
-
?
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
55
|
+
: await inspectRoleStatuses(delivery, candidates, !targetedInventory
|
|
56
|
+
? candidates.flatMap(({ task, role, run, session }) => (isResourceCandidate(task, run, now)
|
|
57
|
+
? [{
|
|
58
|
+
taskId: task.id,
|
|
59
|
+
roleName: role.name,
|
|
60
|
+
runId: run.id,
|
|
61
|
+
agentId: run.effective.agentId,
|
|
62
|
+
adapterId: run.effective.adapterId,
|
|
63
|
+
progressAt: currentRoleRunProgressAt(store, task.id, role.name, run).progressAt,
|
|
64
|
+
...(session?.nativeSessionId === undefined
|
|
65
|
+
? {}
|
|
66
|
+
: { nativeSessionId: session.nativeSessionId }),
|
|
67
|
+
...(session?.launchId === undefined ? {} : { launchId: session.launchId })
|
|
68
|
+
}]
|
|
69
|
+
: []))
|
|
70
|
+
: [], targetedInventory);
|
|
64
71
|
if (liveStatuses !== undefined) {
|
|
65
72
|
for (const [key, status] of batchSnapshot.statuses)
|
|
66
73
|
liveStatuses.set(key, status);
|
|
@@ -106,7 +113,22 @@ export async function reconcileExitedRoleRuns(store, delivery, now, selection, e
|
|
|
106
113
|
}
|
|
107
114
|
return failed;
|
|
108
115
|
}
|
|
109
|
-
async function inspectRoleStatuses(delivery, candidates, resourceInputs) {
|
|
116
|
+
async function inspectRoleStatuses(delivery, candidates, resourceInputs, targeted) {
|
|
117
|
+
// Dirty reconciliation already owns exact Task/Role keys. Probe those keys
|
|
118
|
+
// directly so a concurrent dirty batch does not repeat the provider's
|
|
119
|
+
// global inventory (and its optional process-resource scan) once per Task.
|
|
120
|
+
// Full reconciliation retains the adapter's batch contract below.
|
|
121
|
+
if (targeted && delivery.inspectRole !== undefined) {
|
|
122
|
+
const statuses = new Map();
|
|
123
|
+
for (const candidate of candidates) {
|
|
124
|
+
const key = `${candidate.task.id}\0${candidate.role.name}`;
|
|
125
|
+
if (statuses.has(key)) {
|
|
126
|
+
throw new Error("Tmux Role targeted liveness selection is invalid.");
|
|
127
|
+
}
|
|
128
|
+
statuses.set(key, await delivery.inspectRole(candidate.inspection));
|
|
129
|
+
}
|
|
130
|
+
return { statuses, resources: new Map() };
|
|
131
|
+
}
|
|
110
132
|
if (delivery.inspectRoles !== undefined) {
|
|
111
133
|
return exactBatchInventory(await delivery.inspectRoles(candidates.map(({ inspection }) => inspection), resourceInputs), candidates);
|
|
112
134
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { selectedSchedulerRoles,
|
|
1
|
+
import { selectedSchedulerRoles, selectedActiveSchedulerTasks } from "./ports.js";
|
|
2
2
|
/**
|
|
3
3
|
* Default window of no durable progress before a live-but-idle Run becomes a
|
|
4
4
|
* traceable needs-attention signal. It is deliberately long: a healthy Run that
|
|
@@ -187,7 +187,7 @@ export function latestRunProgressAt(events, runId) {
|
|
|
187
187
|
* the provider-neutral fallback. Resource evidence carries this value as an
|
|
188
188
|
* exact fence, but never advances it.
|
|
189
189
|
*/
|
|
190
|
-
export function currentRoleRunProgressAt(store, taskId, roleName, run, events
|
|
190
|
+
export function currentRoleRunProgressAt(store, taskId, roleName, run, events, progressFacts) {
|
|
191
191
|
let richerProgress;
|
|
192
192
|
try {
|
|
193
193
|
richerProgress = store.getRunDurableProgress?.(taskId, roleName, run.id);
|
|
@@ -209,10 +209,28 @@ export function currentRoleRunProgressAt(store, taskId, roleName, run, events =
|
|
|
209
209
|
: { evidence: richerProgress.evidence })
|
|
210
210
|
};
|
|
211
211
|
}
|
|
212
|
+
// A present fold port is authoritative even when this Run has no entry. Do
|
|
213
|
+
// not evaluate a default history argument or fall back to listEvents in
|
|
214
|
+
// that case: an absent entry is the folded empty fact.
|
|
215
|
+
const folded = store.getRunProgressFacts !== undefined;
|
|
216
|
+
const foldedFacts = folded
|
|
217
|
+
? progressFacts ?? store.getRunProgressFacts?.(taskId, run.id)
|
|
218
|
+
: undefined;
|
|
219
|
+
if (folded) {
|
|
220
|
+
const fallbackProgressAt = latestDurableProgressAt({
|
|
221
|
+
deliveredAt: run.deliveredAt,
|
|
222
|
+
latestCheckpointAt: foldedFacts?.latestCheckpointAt,
|
|
223
|
+
latestActivityAt: foldedFacts?.latestActivityAt
|
|
224
|
+
});
|
|
225
|
+
return { progressAt: fallbackProgressAt };
|
|
226
|
+
}
|
|
227
|
+
// Legacy ports retain the per-Run event-history fallback. Resolve the
|
|
228
|
+
// history only in this branch so fold-backed callers perform zero scans.
|
|
229
|
+
const history = events ?? store.listEvents?.(taskId) ?? [];
|
|
212
230
|
const fallbackProgressAt = latestDurableProgressAt({
|
|
213
231
|
deliveredAt: run.deliveredAt,
|
|
214
|
-
latestCheckpointAt: latestRunProgressAt(
|
|
215
|
-
latestActivityAt: latestRunActivityAt(
|
|
232
|
+
latestCheckpointAt: latestRunProgressAt(history, run.id),
|
|
233
|
+
latestActivityAt: latestRunActivityAt(history, run.id)
|
|
216
234
|
});
|
|
217
235
|
return {
|
|
218
236
|
progressAt: fallbackProgressAt
|
|
@@ -227,13 +245,13 @@ export function latestRunDurableProgressAt(store, taskId, roleName, runId, preco
|
|
|
227
245
|
const run = store.getAgentRun(taskId, runId);
|
|
228
246
|
if (run === null || run.taskId !== taskId || run.roleName !== roleName)
|
|
229
247
|
return null;
|
|
230
|
-
const events = store.listEvents(taskId);
|
|
231
248
|
// The adapter folds checkpoint/activity once per revision. When the fold
|
|
232
249
|
// port exists, a missing per-Run entry is an authoritative empty fold: use
|
|
233
250
|
// its (possibly undefined) values directly and never re-scan the whole
|
|
234
251
|
// history per candidate. Legacy callers without the port omit precomputed
|
|
235
252
|
// and keep the per-candidate full-history scans.
|
|
236
253
|
const folded = precomputed !== undefined;
|
|
254
|
+
const events = folded ? undefined : store.listEvents(taskId);
|
|
237
255
|
const latestCheckpointAt = folded
|
|
238
256
|
? precomputed.latestCheckpointAt
|
|
239
257
|
: latestRunProgressAt(events, run.id);
|
|
@@ -480,26 +498,25 @@ export async function reconcileStalledRoleRuns(store, delivery, now, selection,
|
|
|
480
498
|
// existing mailbox work without manufacturing another episode.
|
|
481
499
|
if (selection !== undefined && !selection.full)
|
|
482
500
|
return [];
|
|
483
|
-
if (store.
|
|
501
|
+
if ((store.getRunProgressFacts === undefined && store.listEvents === undefined)
|
|
502
|
+
|| store.recordRoleRunStall === undefined)
|
|
484
503
|
return [];
|
|
485
504
|
// When the fold port exists, a missing per-Run entry is an authoritative
|
|
486
505
|
// empty fold: the stall reconciliation must not re-scan the whole history
|
|
487
506
|
// per candidate. Legacy stores without the port keep the per-candidate
|
|
488
507
|
// scans.
|
|
489
508
|
const foldPortPresent = store.getRunProgressFacts !== undefined;
|
|
490
|
-
const candidates =
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
}];
|
|
502
|
-
})));
|
|
509
|
+
const candidates = selectedActiveSchedulerTasks(store, selection).flatMap((task) => (selectedSchedulerRoles(store, task.id, selection).flatMap((role) => {
|
|
510
|
+
const run = store.getActiveAgentRun(task.id, role.name);
|
|
511
|
+
if (run === null || run.status !== "active")
|
|
512
|
+
return [];
|
|
513
|
+
return [{
|
|
514
|
+
task,
|
|
515
|
+
role,
|
|
516
|
+
run,
|
|
517
|
+
session: store.getRoleSession(task.id, role.name, run.effective.agentId)
|
|
518
|
+
}];
|
|
519
|
+
})));
|
|
503
520
|
const stallCandidates = candidates.filter(({ run }) => isStallCandidate(run, now, windowMs));
|
|
504
521
|
if (stallCandidates.length === 0)
|
|
505
522
|
return [];
|
|
@@ -617,15 +634,22 @@ export async function reconcileStalledRoleRuns(store, delivery, now, selection,
|
|
|
617
634
|
});
|
|
618
635
|
continue;
|
|
619
636
|
}
|
|
620
|
-
|
|
621
|
-
|
|
637
|
+
// Fold-backed adapters expose an authoritative per-Run fact, so do not
|
|
638
|
+
// load the Task history just to satisfy currentRoleRunProgressAt or its
|
|
639
|
+
// fallback. Legacy ports retain one history read for these projections.
|
|
640
|
+
const progressFacts = foldPortPresent
|
|
641
|
+
? store.getRunProgressFacts?.(candidate.task.id, candidate.run.id)
|
|
642
|
+
: undefined;
|
|
643
|
+
const events = foldPortPresent
|
|
644
|
+
? undefined
|
|
645
|
+
: store.listEvents?.(candidate.task.id);
|
|
622
646
|
// Before exact acceptance there is no execution progress clock. Keep the
|
|
623
647
|
// delivery watch anchored to the Run creation/transport boundary even if
|
|
624
648
|
// checkpoints, output, or related WorkItem/Review/Integration records are
|
|
625
649
|
// newer; those facts are useful evidence but cannot prove provider
|
|
626
650
|
// acceptance or reset delivery timeout. Once accepted, deliveredAt is the
|
|
627
651
|
// semantic baseline and the durable fold may advance it.
|
|
628
|
-
const progress = currentRoleRunProgressAt(store, candidate.task.id, candidate.role.name, candidate.run, events);
|
|
652
|
+
const progress = currentRoleRunProgressAt(store, candidate.task.id, candidate.role.name, candidate.run, events, progressFacts);
|
|
629
653
|
const progressAt = progress.progressAt;
|
|
630
654
|
const evaluation = evaluateRoleRunStall({
|
|
631
655
|
progressAt,
|
|
@@ -633,7 +657,7 @@ export async function reconcileStalledRoleRuns(store, delivery, now, selection,
|
|
|
633
657
|
windowMs,
|
|
634
658
|
lastAttentionProgressAt: foldPortPresent
|
|
635
659
|
? progressFacts?.latestStall?.progressAt
|
|
636
|
-
: latestStallProgressAt(events, candidate.run.id)
|
|
660
|
+
: latestStallProgressAt(events ?? [], candidate.run.id)
|
|
637
661
|
});
|
|
638
662
|
const runAgentId = candidate.run.effective.agentId;
|
|
639
663
|
const runAdapterId = candidate.run.effective.adapterId;
|
|
@@ -710,7 +734,7 @@ export async function reconcileStalledRoleRuns(store, delivery, now, selection,
|
|
|
710
734
|
const { candidate, progressAt } = current;
|
|
711
735
|
const previous = foldPortPresent
|
|
712
736
|
? store.getRunProgressFacts?.(candidate.task.id, candidate.run.id)?.latestStall
|
|
713
|
-
: latestStallEvidenceKey(store.listEvents(candidate.task.id), candidate.run.id);
|
|
737
|
+
: latestStallEvidenceKey(store.listEvents?.(candidate.task.id) ?? [], candidate.run.id);
|
|
714
738
|
if (previous !== undefined
|
|
715
739
|
&& Date.parse(progressAt) > Date.parse(previous.progressAt)) {
|
|
716
740
|
// A new semantic progress point closes the previous episode first. It
|
|
@@ -784,7 +808,7 @@ export async function reconcileStalledRoleRuns(store, delivery, now, selection,
|
|
|
784
808
|
const { candidate, progressAt } = current;
|
|
785
809
|
const previous = foldPortPresent
|
|
786
810
|
? store.getRunProgressFacts?.(candidate.task.id, candidate.run.id)?.latestStall
|
|
787
|
-
: latestStallEvidenceKey(store.listEvents(candidate.task.id), candidate.run.id);
|
|
811
|
+
: latestStallEvidenceKey(store.listEvents?.(candidate.task.id) ?? [], candidate.run.id);
|
|
788
812
|
if (previous !== undefined
|
|
789
813
|
&& Date.parse(progressAt) > Date.parse(previous.progressAt)) {
|
|
790
814
|
store.recordRoleRunProgress?.({
|