@abelxiaoxing/cadence 1.4.0 → 1.5.0
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 +130 -8
- package/agents/design-explorer.md +17 -11
- package/agents/diagnosis-worker.md +9 -1
- package/agents/implementation-worker.md +3 -1
- package/config/plan-draft.example.json +122 -0
- package/config/plan-draft.multiple-tasks.example.json +345 -0
- package/config/plan-draft.quick.example.json +78 -0
- package/package.json +10 -4
- package/prompts/abel-design.md +101 -44
- package/prompts/abel-implement.md +9 -0
- package/src/activity-contracts.ts +82 -0
- package/src/candidate-artifact.ts +143 -0
- package/src/candidate-patch.ts +31 -11
- package/src/candidate-workspace.ts +74 -0
- package/src/change-contract-schema.ts +191 -0
- package/src/change-contract.ts +5 -0
- package/src/change-verification.ts +920 -0
- package/src/child-budget.ts +58 -0
- package/src/child-model.ts +142 -0
- package/src/child-session.ts +289 -326
- package/src/contracts.ts +200 -76
- package/src/delivery-compiler.ts +105 -120
- package/src/delivery-revision.ts +42 -0
- package/src/design-control.ts +113 -4
- package/src/design-diagnostics.ts +150 -0
- package/src/durable-contracts.ts +213 -0
- package/src/durable-workflow.ts +148 -3570
- package/src/evidence-draft.ts +122 -0
- package/src/execution-profile.ts +38 -0
- package/src/implement-graph.ts +3 -0
- package/src/implement-plan.ts +70 -0
- package/src/index.ts +59 -522
- package/src/isolation-backend.ts +62 -40
- package/src/model-source.ts +22 -0
- package/src/operator-cli-entry.mjs +24 -0
- package/src/operator-cli.mjs +1421 -0
- package/src/operator-tools.ts +249 -0
- package/src/package-candidate.ts +227 -0
- package/src/package-verification.ts +86 -8
- package/src/package-workflow.ts +291 -0
- package/src/packet-runtime.ts +27 -48
- package/src/parent-provider.ts +86 -198
- package/src/phase-execution.ts +2248 -0
- package/src/pi-adapter.ts +19 -0
- package/src/plan-draft-summary.ts +149 -0
- package/src/plan-draft.ts +493 -0
- package/src/single-task-draft.ts +142 -0
- package/src/subagent-activity.ts +32 -66
- package/src/submit-tool.ts +20 -59
- package/src/transport-budget.ts +52 -0
- package/src/verification-capability.ts +5 -3
- package/src/verification-diagnostics.ts +76 -0
- package/src/verification-environment-io.ts +280 -0
- package/src/verification-environment.ts +56 -154
- package/src/verification-scan.ts +64 -0
- package/src/worker-broker.ts +39 -57
- package/src/workflow-policy.ts +14 -53
- package/src/workflow-recovery-policy.ts +95 -0
- package/src/workflow-scheduling.ts +105 -0
- package/src/workflow-state-machine.ts +105 -497
- package/src/workflow-status.ts +384 -0
- package/src/workspace-io-entry.mjs +19 -1
- package/src/workspace-io-worker.mjs +275 -50
- package/src/workspace-io.ts +6 -1
- package/src/empty-resource-loader.ts +0 -38
- package/src/parent-payload-bridge.ts +0 -523
package/src/worker-broker.ts
CHANGED
|
@@ -5,11 +5,9 @@ import {
|
|
|
5
5
|
type WorkerRole,
|
|
6
6
|
type WorkerRoutePolicy,
|
|
7
7
|
} from "./route-policy.ts";
|
|
8
|
+
import { TransportTimeout } from "./transport-budget.ts";
|
|
8
9
|
|
|
9
10
|
export const ROUTE_ATTEMPT_BOUNDS = Object.freeze({
|
|
10
|
-
connectMs: 10_000,
|
|
11
|
-
firstResponseMs: 90_000,
|
|
12
|
-
idleMs: 3 * 60_000,
|
|
13
11
|
totalMs: 20 * 60_000,
|
|
14
12
|
cooldownMs: 30_000,
|
|
15
13
|
attemptsPerRoute: 2,
|
|
@@ -71,6 +69,7 @@ export interface RunWorkerAttemptInput<T> {
|
|
|
71
69
|
export interface RouteAttemptContext {
|
|
72
70
|
route: WorkerRoutePolicy;
|
|
73
71
|
signal: AbortSignal;
|
|
72
|
+
onRequestStart(): void;
|
|
74
73
|
onHeaders(): void;
|
|
75
74
|
onProgress(): void;
|
|
76
75
|
}
|
|
@@ -81,7 +80,7 @@ export interface BrokerAttemptEvidence {
|
|
|
81
80
|
}
|
|
82
81
|
|
|
83
82
|
export interface BrokerActivityUpdate {
|
|
84
|
-
state: "
|
|
83
|
+
state: "preparing" | "waiting-first-response" | "running" | "retrying";
|
|
85
84
|
attempt: number;
|
|
86
85
|
maxAttempts: number;
|
|
87
86
|
code?: string;
|
|
@@ -100,12 +99,7 @@ function emitBrokerActivity(
|
|
|
100
99
|
}
|
|
101
100
|
|
|
102
101
|
class RouteAttemptStop extends Error {
|
|
103
|
-
readonly code:
|
|
104
|
-
| "connect-timeout"
|
|
105
|
-
| "first-response-timeout"
|
|
106
|
-
| "idle-timeout"
|
|
107
|
-
| "phase-timeout"
|
|
108
|
-
| "cancelled";
|
|
102
|
+
readonly code: "cancelled";
|
|
109
103
|
|
|
110
104
|
constructor(code: RouteAttemptStop["code"]) {
|
|
111
105
|
super(code);
|
|
@@ -182,7 +176,10 @@ function capable(
|
|
|
182
176
|
}
|
|
183
177
|
|
|
184
178
|
function safeFailureCode(error: unknown): string {
|
|
185
|
-
if (error instanceof RouteAttemptStop
|
|
179
|
+
if (error instanceof RouteAttemptStop || error instanceof TransportTimeout)
|
|
180
|
+
return error.code;
|
|
181
|
+
if (error instanceof Error && error.message === "child-provider-rate-limited")
|
|
182
|
+
return error.message;
|
|
186
183
|
return "transport-failure";
|
|
187
184
|
}
|
|
188
185
|
|
|
@@ -331,59 +328,39 @@ export class WorkerBroker {
|
|
|
331
328
|
onActivity?: (state: "waiting-first-response" | "running") => void,
|
|
332
329
|
): Promise<T> {
|
|
333
330
|
const controller = new AbortController();
|
|
334
|
-
|
|
335
|
-
let
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const stop = (code: RouteAttemptStop["code"]) => {
|
|
341
|
-
if (!controller.signal.aborted)
|
|
342
|
-
controller.abort(new RouteAttemptStop(code));
|
|
343
|
-
};
|
|
344
|
-
const schedule = (code: RouteAttemptStop["code"], milliseconds: number) => {
|
|
345
|
-
const timer = setTimeout(() => stop(code), milliseconds);
|
|
346
|
-
timers.add(timer);
|
|
347
|
-
return timer;
|
|
348
|
-
};
|
|
349
|
-
const clear = (timer: ReturnType<typeof setTimeout> | undefined) => {
|
|
350
|
-
if (timer !== undefined) {
|
|
351
|
-
clearTimeout(timer);
|
|
352
|
-
timers.delete(timer);
|
|
353
|
-
}
|
|
354
|
-
};
|
|
355
|
-
const onParentAbort = () => stop("cancelled");
|
|
356
|
-
if (parentSignal?.aborted) stop("cancelled");
|
|
331
|
+
let accepting = true;
|
|
332
|
+
let running = false;
|
|
333
|
+
const onParentAbort = () =>
|
|
334
|
+
controller.abort(new RouteAttemptStop("cancelled"));
|
|
335
|
+
if (parentSignal?.aborted) onParentAbort();
|
|
357
336
|
else parentSignal?.addEventListener("abort", onParentAbort, { once: true });
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
337
|
+
const timer = setTimeout(
|
|
338
|
+
() => controller.abort(new TransportTimeout("attempt-timeout")),
|
|
339
|
+
ROUTE_ATTEMPT_BOUNDS.totalMs,
|
|
340
|
+
);
|
|
361
341
|
let removeAbortRace: () => void = () => {};
|
|
362
342
|
try {
|
|
363
|
-
const execution = Promise.resolve().then(() =>
|
|
364
|
-
|
|
343
|
+
const execution = Promise.resolve().then(() => {
|
|
344
|
+
controller.signal.throwIfAborted();
|
|
345
|
+
return execute({
|
|
365
346
|
route: structuredClone(route),
|
|
366
347
|
signal: controller.signal,
|
|
348
|
+
onRequestStart: () => {
|
|
349
|
+
if (!accepting || controller.signal.aborted) return;
|
|
350
|
+
running = false;
|
|
351
|
+
onActivity?.("waiting-first-response");
|
|
352
|
+
},
|
|
367
353
|
onHeaders: () => {
|
|
368
|
-
if (controller.signal.aborted ||
|
|
369
|
-
headersSeen = true;
|
|
370
|
-
clear(connectTimer);
|
|
354
|
+
if (!accepting || controller.signal.aborted || running) return;
|
|
371
355
|
onActivity?.("waiting-first-response");
|
|
372
|
-
firstResponseTimer = schedule(
|
|
373
|
-
"first-response-timeout",
|
|
374
|
-
ROUTE_ATTEMPT_BOUNDS.firstResponseMs,
|
|
375
|
-
);
|
|
376
356
|
},
|
|
377
357
|
onProgress: () => {
|
|
378
|
-
if (controller.signal.aborted) return;
|
|
379
|
-
|
|
380
|
-
clear(firstResponseTimer);
|
|
381
|
-
clear(idleTimer);
|
|
382
|
-
idleTimer = schedule("idle-timeout", ROUTE_ATTEMPT_BOUNDS.idleMs);
|
|
358
|
+
if (!accepting || controller.signal.aborted) return;
|
|
359
|
+
running = true;
|
|
383
360
|
onActivity?.("running");
|
|
384
361
|
},
|
|
385
|
-
})
|
|
386
|
-
);
|
|
362
|
+
});
|
|
363
|
+
});
|
|
387
364
|
// A timed-out Provider may ignore its AbortSignal and settle later. The
|
|
388
365
|
// broker owns the bounded result, so absorb that late settlement.
|
|
389
366
|
void execution.catch(() => undefined);
|
|
@@ -403,7 +380,8 @@ export class WorkerBroker {
|
|
|
403
380
|
return await Promise.race([execution, aborted]);
|
|
404
381
|
} finally {
|
|
405
382
|
removeAbortRace();
|
|
406
|
-
|
|
383
|
+
accepting = false;
|
|
384
|
+
clearTimeout(timer);
|
|
407
385
|
parentSignal?.removeEventListener("abort", onParentAbort);
|
|
408
386
|
}
|
|
409
387
|
}
|
|
@@ -483,10 +461,10 @@ export class WorkerBroker {
|
|
|
483
461
|
).length;
|
|
484
462
|
const attempt = evidence.length + 1;
|
|
485
463
|
emitBrokerActivity(input.onActivity, {
|
|
486
|
-
state: "
|
|
464
|
+
state: "preparing",
|
|
487
465
|
attempt,
|
|
488
466
|
maxAttempts,
|
|
489
|
-
wait: "
|
|
467
|
+
wait: "worker-setup",
|
|
490
468
|
});
|
|
491
469
|
try {
|
|
492
470
|
const value = await this.#attempt(
|
|
@@ -576,6 +554,7 @@ export class WorkerBroker {
|
|
|
576
554
|
code:
|
|
577
555
|
| "approval-boundary"
|
|
578
556
|
| "route-not-declared"
|
|
557
|
+
| "endpoint-unavailable"
|
|
579
558
|
| "route-capability-insufficient";
|
|
580
559
|
} {
|
|
581
560
|
const allowed = new Set(["runId", "role", "routeId", "requirements"]);
|
|
@@ -597,12 +576,15 @@ export class WorkerBroker {
|
|
|
597
576
|
routeId: input.routeId,
|
|
598
577
|
});
|
|
599
578
|
if (!selection.ok) {
|
|
579
|
+
const route = this.#policy.routes[input.routeId];
|
|
600
580
|
return {
|
|
601
581
|
ok: false,
|
|
602
582
|
code:
|
|
603
583
|
selection.code === "route-not-declared"
|
|
604
584
|
? "route-not-declared"
|
|
605
|
-
:
|
|
585
|
+
: route && capable(route, input.role, input.requirements ?? {})
|
|
586
|
+
? "endpoint-unavailable"
|
|
587
|
+
: "route-capability-insufficient",
|
|
606
588
|
};
|
|
607
589
|
}
|
|
608
590
|
return {
|
package/src/workflow-policy.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import type { WorkflowActivityUpdate } from "./activity-contracts.ts";
|
|
4
5
|
import { compareCanonicalStrings } from "./canonical.ts";
|
|
5
6
|
import {
|
|
6
7
|
type ApprovalBoundaryCode,
|
|
@@ -18,7 +19,6 @@ import type { RoutePolicy } from "./route-policy.ts";
|
|
|
18
19
|
import { canonicalJson } from "./run-state.ts";
|
|
19
20
|
import type { OperationLease } from "./run-store.ts";
|
|
20
21
|
import type { ResolvedStateRoot } from "./state-root.ts";
|
|
21
|
-
import type { WorkflowActivityUpdate } from "./subagent-activity.ts";
|
|
22
22
|
import {
|
|
23
23
|
type CandidateContextBoundary,
|
|
24
24
|
type CandidateContextRef,
|
|
@@ -185,59 +185,20 @@ export interface RedArtifactCorrection {
|
|
|
185
185
|
contextRequest?: WorkflowContextRequest;
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
export type
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
allowed: false;
|
|
196
|
-
code: "repair-attempts-exhausted" | "artifact-attempts-exhausted";
|
|
197
|
-
};
|
|
188
|
+
export type {
|
|
189
|
+
RecoveryActionDecision,
|
|
190
|
+
RecoveryActionRequest,
|
|
191
|
+
WorkflowRecoveryFact,
|
|
192
|
+
WorkflowRecoveryFeedback,
|
|
193
|
+
} from "./workflow-recovery-policy.ts";
|
|
194
|
+
export { decideRecoveryAction } from "./workflow-recovery-policy.ts";
|
|
198
195
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const correction = request.kind === "red-correction";
|
|
206
|
-
const limit =
|
|
207
|
-
authority.additionalAttempt || authority.verificationOnly
|
|
208
|
-
? 1
|
|
209
|
-
: correction
|
|
210
|
-
? plan.verification.artifactCorrection.maxAttempts
|
|
211
|
-
: plan.verification.repair.maxAttempts;
|
|
212
|
-
return Number.isSafeInteger(request.attempt) &&
|
|
213
|
-
request.attempt > 0 &&
|
|
214
|
-
request.attempt <= limit
|
|
215
|
-
? { allowed: true }
|
|
216
|
-
: {
|
|
217
|
-
allowed: false,
|
|
218
|
-
code: correction
|
|
219
|
-
? "artifact-attempts-exhausted"
|
|
220
|
-
: "repair-attempts-exhausted",
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
export interface WorkflowRecoveryFeedback {
|
|
225
|
-
code: string;
|
|
226
|
-
attempt: number;
|
|
227
|
-
maxAttempts: number;
|
|
228
|
-
strategy:
|
|
229
|
-
| "revise-candidate"
|
|
230
|
-
| "refresh-candidate"
|
|
231
|
-
| "repair-verification"
|
|
232
|
-
| "compact-patch";
|
|
233
|
-
failureIdentities?: string[];
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
export interface WorkflowRecoveryFact {
|
|
237
|
-
key: string;
|
|
238
|
-
failures: number;
|
|
239
|
-
feedback: WorkflowRecoveryFeedback;
|
|
240
|
-
}
|
|
196
|
+
import type {
|
|
197
|
+
RecoveryActionDecision,
|
|
198
|
+
RecoveryActionRequest,
|
|
199
|
+
WorkflowRecoveryFact,
|
|
200
|
+
WorkflowRecoveryFeedback,
|
|
201
|
+
} from "./workflow-recovery-policy.ts";
|
|
241
202
|
|
|
242
203
|
export const APPROVAL_AUTHORITY_CATEGORIES = [
|
|
243
204
|
"observable-behavior",
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { ImplementPlan } from "./implement-plan.ts";
|
|
2
|
+
|
|
3
|
+
export type RecoveryActionRequest = {
|
|
4
|
+
kind: "affected-repair" | "cumulative-repair" | "red-correction";
|
|
5
|
+
attempt: number;
|
|
6
|
+
};
|
|
7
|
+
export type RecoveryActionDecision =
|
|
8
|
+
| { allowed: true }
|
|
9
|
+
| {
|
|
10
|
+
allowed: false;
|
|
11
|
+
code: "repair-attempts-exhausted" | "artifact-attempts-exhausted";
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** One policy for every nested recovery action; execution services supply facts only. */
|
|
15
|
+
export function decideRecoveryAction(
|
|
16
|
+
plan: ImplementPlan,
|
|
17
|
+
request: RecoveryActionRequest,
|
|
18
|
+
authority: { additionalAttempt?: boolean; verificationOnly?: boolean } = {},
|
|
19
|
+
): RecoveryActionDecision {
|
|
20
|
+
const correction = request.kind === "red-correction";
|
|
21
|
+
const limit =
|
|
22
|
+
authority.additionalAttempt || authority.verificationOnly
|
|
23
|
+
? 1
|
|
24
|
+
: correction
|
|
25
|
+
? plan.verification.artifactCorrection.maxAttempts
|
|
26
|
+
: plan.verification.repair.maxAttempts;
|
|
27
|
+
return Number.isSafeInteger(request.attempt) &&
|
|
28
|
+
request.attempt > 0 &&
|
|
29
|
+
request.attempt <= limit
|
|
30
|
+
? { allowed: true }
|
|
31
|
+
: {
|
|
32
|
+
allowed: false,
|
|
33
|
+
code: correction
|
|
34
|
+
? "artifact-attempts-exhausted"
|
|
35
|
+
: "repair-attempts-exhausted",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface WorkflowRecoveryFeedback {
|
|
40
|
+
code: string;
|
|
41
|
+
attempt: number;
|
|
42
|
+
maxAttempts: number;
|
|
43
|
+
strategy:
|
|
44
|
+
| "revise-candidate"
|
|
45
|
+
| "refresh-candidate"
|
|
46
|
+
| "repair-verification"
|
|
47
|
+
| "compact-patch";
|
|
48
|
+
failureIdentities?: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface WorkflowRecoveryFact {
|
|
52
|
+
key: string;
|
|
53
|
+
failures: number;
|
|
54
|
+
feedback: WorkflowRecoveryFeedback;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function recoveryExhausted(
|
|
58
|
+
incident: WorkflowRecoveryFact | undefined,
|
|
59
|
+
maximum: number,
|
|
60
|
+
granted: boolean,
|
|
61
|
+
): boolean {
|
|
62
|
+
return !granted && incident !== undefined && incident.failures >= maximum;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A grant is a recommendation until the state machine validates and records it in its lease transaction. */
|
|
66
|
+
export function assessRecoveryGrant(input: {
|
|
67
|
+
current: boolean;
|
|
68
|
+
incident?: WorkflowRecoveryFact;
|
|
69
|
+
failureSequence: number;
|
|
70
|
+
requestedSequence: number;
|
|
71
|
+
reason: string;
|
|
72
|
+
previous: { route?: string | null; context?: string | null };
|
|
73
|
+
observed: { route?: string | null; context?: string | null };
|
|
74
|
+
budget?: { used: number; max_work: number };
|
|
75
|
+
}): string | undefined {
|
|
76
|
+
if (
|
|
77
|
+
!input.current ||
|
|
78
|
+
!input.incident ||
|
|
79
|
+
input.incident.failures < input.incident.feedback.maxAttempts ||
|
|
80
|
+
input.failureSequence !== input.requestedSequence
|
|
81
|
+
)
|
|
82
|
+
return "recovery-request-stale";
|
|
83
|
+
if (
|
|
84
|
+
(input.reason === "route-changed" &&
|
|
85
|
+
(!input.previous.route ||
|
|
86
|
+
input.previous.route === input.observed.route)) ||
|
|
87
|
+
(input.reason === "context-extended" &&
|
|
88
|
+
(!input.previous.context ||
|
|
89
|
+
input.previous.context === input.observed.context))
|
|
90
|
+
)
|
|
91
|
+
return "recovery-evidence-unavailable";
|
|
92
|
+
if (!input.budget || input.budget.used >= input.budget.max_work)
|
|
93
|
+
return "change-work-budget-exhausted";
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { PlanTaskDraft } from "./implement-plan.ts";
|
|
2
|
+
import { type EngineTaskRow, taskConflicts } from "./workflow-policy.ts";
|
|
3
|
+
|
|
4
|
+
function taskPlan(row: EngineTaskRow): PlanTaskDraft {
|
|
5
|
+
return JSON.parse(row.plan_json) as PlanTaskDraft;
|
|
6
|
+
}
|
|
7
|
+
export function hasTaskConflict(
|
|
8
|
+
row: EngineTaskRow,
|
|
9
|
+
rows: EngineTaskRow[],
|
|
10
|
+
selectedTaskIds: ReadonlySet<string>,
|
|
11
|
+
): boolean {
|
|
12
|
+
const candidate = taskPlan(row);
|
|
13
|
+
return rows.some((other) => {
|
|
14
|
+
if (other.task_id === row.task_id || other.state === "verified") {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
if (other.state === "queued") {
|
|
18
|
+
if (row.state !== "queued") return false;
|
|
19
|
+
const candidatePosition = row.queue_position ?? Number.MAX_SAFE_INTEGER;
|
|
20
|
+
const otherPosition = other.queue_position ?? Number.MAX_SAFE_INTEGER;
|
|
21
|
+
if (
|
|
22
|
+
otherPosition > candidatePosition ||
|
|
23
|
+
(otherPosition === candidatePosition &&
|
|
24
|
+
other.task_order > row.task_order)
|
|
25
|
+
) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (other.task_order > row.task_order && other.state === "pending") {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (other.state === "pending" && !dependenciesVerified(other, rows)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const conflict = taskConflicts(candidate, taskPlan(other));
|
|
36
|
+
return (
|
|
37
|
+
conflict.taskLifetime ||
|
|
38
|
+
(conflict.verification &&
|
|
39
|
+
([
|
|
40
|
+
"pending",
|
|
41
|
+
"phase-ready",
|
|
42
|
+
"phase-running",
|
|
43
|
+
"validating",
|
|
44
|
+
"queued",
|
|
45
|
+
].includes(other.state) ||
|
|
46
|
+
selectedTaskIds.has(other.task_id)))
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function dependenciesVerified(
|
|
52
|
+
row: EngineTaskRow,
|
|
53
|
+
rows: EngineTaskRow[],
|
|
54
|
+
): boolean {
|
|
55
|
+
const byId = new Map(rows.map((candidate) => [candidate.task_id, candidate]));
|
|
56
|
+
return taskPlan(row).dependsOn.every(
|
|
57
|
+
(dependency) => byId.get(dependency)?.state === "verified",
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Simulate ordered queue decisions without storage access or launch authority. */
|
|
62
|
+
export function selectRunnableTasks(input: {
|
|
63
|
+
rows: readonly EngineTaskRow[];
|
|
64
|
+
attempted: ReadonlySet<string>;
|
|
65
|
+
activeTasks: number;
|
|
66
|
+
capacity: number;
|
|
67
|
+
nextQueuePosition: number;
|
|
68
|
+
}) {
|
|
69
|
+
const rows = input.rows.map((row) => ({ ...row }));
|
|
70
|
+
const runnable: string[] = [];
|
|
71
|
+
const selected = new Set<string>();
|
|
72
|
+
const updates: Array<
|
|
73
|
+
| { kind: "pending"; taskId: string }
|
|
74
|
+
| { kind: "queue"; taskId: string; reason: "conflict" | "capacity" }
|
|
75
|
+
> = [];
|
|
76
|
+
let position = input.nextQueuePosition;
|
|
77
|
+
let capacityBlocked = false;
|
|
78
|
+
for (const row of rows) {
|
|
79
|
+
if (row.state === "verified" || input.attempted.has(row.task_id)) continue;
|
|
80
|
+
if (!dependenciesVerified(row, rows)) {
|
|
81
|
+
if (row.state !== "pending") {
|
|
82
|
+
updates.push({ kind: "pending", taskId: row.task_id });
|
|
83
|
+
row.state = "pending";
|
|
84
|
+
row.pause_code = null;
|
|
85
|
+
row.queue_position = null;
|
|
86
|
+
}
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const conflict = hasTaskConflict(row, rows, selected);
|
|
90
|
+
const capacity = input.activeTasks + runnable.length >= input.capacity;
|
|
91
|
+
if (conflict || capacity) {
|
|
92
|
+
const reason = conflict ? "conflict" : "capacity";
|
|
93
|
+
updates.push({ kind: "queue", taskId: row.task_id, reason });
|
|
94
|
+
if (row.state !== "queued" || row.queue_position === null)
|
|
95
|
+
row.queue_position = position++;
|
|
96
|
+
row.state = "queued";
|
|
97
|
+
row.pause_code = reason === "capacity" ? "worker-capacity" : null;
|
|
98
|
+
if (!conflict) capacityBlocked = true;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
runnable.push(row.task_id);
|
|
102
|
+
selected.add(row.task_id);
|
|
103
|
+
}
|
|
104
|
+
return { runnable, updates, capacityBlocked };
|
|
105
|
+
}
|