@abelxiaoxing/cadence 1.0.0 → 1.0.1
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 +50 -22
- package/package.json +3 -1
- package/prompts/abel-design.md +1 -0
- package/prompts/abel-diagnose.md +1 -0
- package/prompts/abel-implement.md +1 -0
- package/src/child-session.ts +53 -6
- package/src/index.ts +144 -12
- package/src/parent-provider.ts +36 -7
- package/src/runtime.ts +262 -18
- package/src/scheduler.ts +46 -10
- package/src/subagent-activity.ts +747 -0
package/src/runtime.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { type Bound, mergeBounds } from "./file-snapshot.ts";
|
|
|
14
14
|
import { runtimeFromContext } from "./parent-provider.ts";
|
|
15
15
|
import { applyRetainedPatch } from "./patch.ts";
|
|
16
16
|
import { ResultStore } from "./result-store.ts";
|
|
17
|
+
import { Scheduler } from "./scheduler.ts";
|
|
17
18
|
import {
|
|
18
19
|
contractOf,
|
|
19
20
|
sameContract,
|
|
@@ -23,6 +24,51 @@ import {
|
|
|
23
24
|
|
|
24
25
|
const SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
25
26
|
|
|
27
|
+
function cancellationError(signal: AbortSignal): { ok: false; error: string } {
|
|
28
|
+
const reason = signal.reason;
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
error:
|
|
32
|
+
reason instanceof Error
|
|
33
|
+
? reason.message
|
|
34
|
+
: typeof reason === "string"
|
|
35
|
+
? reason
|
|
36
|
+
: "child phase cancelled",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type RunFailureKind = "failed" | "cancelled" | "timed-out";
|
|
41
|
+
export type RuntimeFailureReason =
|
|
42
|
+
| "subagent failed"
|
|
43
|
+
| "subagent cancelled"
|
|
44
|
+
| "phase timed out";
|
|
45
|
+
export type RuntimeActivityState =
|
|
46
|
+
| "queued"
|
|
47
|
+
| "running"
|
|
48
|
+
| "completed"
|
|
49
|
+
| "failed"
|
|
50
|
+
| "cancelled"
|
|
51
|
+
| "timed-out";
|
|
52
|
+
|
|
53
|
+
export interface RuntimeActivityEvent {
|
|
54
|
+
state: RuntimeActivityState;
|
|
55
|
+
requestId: string;
|
|
56
|
+
role: string;
|
|
57
|
+
phase: string;
|
|
58
|
+
objective: string;
|
|
59
|
+
sequence: number;
|
|
60
|
+
failureReason?: RuntimeFailureReason;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type RuntimeActivityObserver = (
|
|
64
|
+
event: RuntimeActivityEvent,
|
|
65
|
+
) => void | Promise<void>;
|
|
66
|
+
|
|
67
|
+
interface ScheduledRunResult {
|
|
68
|
+
dispatch: DispatchResult;
|
|
69
|
+
failureKind?: RunFailureKind;
|
|
70
|
+
}
|
|
71
|
+
|
|
26
72
|
function isSafeBound(snapshot: unknown): snapshot is Bound {
|
|
27
73
|
if (
|
|
28
74
|
typeof snapshot !== "object" ||
|
|
@@ -92,22 +138,53 @@ export interface RuntimeOptions {
|
|
|
92
138
|
|
|
93
139
|
type RunContext = Pick<ExtensionContext, "cwd" | "model" | "modelRegistry">;
|
|
94
140
|
|
|
141
|
+
interface StoredRunContext {
|
|
142
|
+
ctx: RunContext;
|
|
143
|
+
observer?: RuntimeActivityObserver;
|
|
144
|
+
sequence: number;
|
|
145
|
+
runningEmitted: boolean;
|
|
146
|
+
terminalEmitted: boolean;
|
|
147
|
+
}
|
|
148
|
+
|
|
95
149
|
export class Runtime {
|
|
96
150
|
readonly activation: Activation;
|
|
97
151
|
readonly limits = LIMITS;
|
|
98
152
|
readonly results = new ResultStore();
|
|
99
153
|
private applyTail: Promise<void> = Promise.resolve();
|
|
100
154
|
private applySeq = 0;
|
|
155
|
+
private batchSeq = 0;
|
|
101
156
|
private readonly registry = new WorkerRegistry();
|
|
157
|
+
private readonly runContexts = new WeakMap<
|
|
158
|
+
RequestEnvelope,
|
|
159
|
+
StoredRunContext
|
|
160
|
+
>();
|
|
161
|
+
private readonly scheduler: Scheduler<ScheduledRunResult>;
|
|
102
162
|
|
|
103
163
|
constructor(opts: RuntimeOptions = {}) {
|
|
104
164
|
this.activation = opts.activation ?? new Activation();
|
|
165
|
+
this.scheduler = new Scheduler({
|
|
166
|
+
limit: LIMITS.maxActiveChildSessions,
|
|
167
|
+
execute: async (request, signal) => {
|
|
168
|
+
const context = this.runContexts.get(request);
|
|
169
|
+
if (!context) throw new Error("scheduled run context is unavailable");
|
|
170
|
+
this.notify(request, context, "running");
|
|
171
|
+
const result = await this.runScheduled(request, context.ctx, signal);
|
|
172
|
+
return {
|
|
173
|
+
dispatch: result,
|
|
174
|
+
...(result.failureKind === undefined
|
|
175
|
+
? {}
|
|
176
|
+
: { failureKind: result.failureKind }),
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
});
|
|
105
180
|
}
|
|
106
181
|
|
|
107
182
|
async execute(
|
|
108
183
|
action: string,
|
|
109
184
|
params: { request?: unknown; resultId?: string },
|
|
110
185
|
ctx?: RunContext,
|
|
186
|
+
signal?: AbortSignal,
|
|
187
|
+
observer?: RuntimeActivityObserver,
|
|
111
188
|
): Promise<DispatchResult> {
|
|
112
189
|
if (!(ACTIONS as readonly string[]).includes(action)) {
|
|
113
190
|
return { ok: false, error: `unknown action: ${String(action)}` };
|
|
@@ -115,7 +192,8 @@ export class Runtime {
|
|
|
115
192
|
if (!this.activation.isActive()) {
|
|
116
193
|
return { ok: false, notReady: true, error: "dispatcher is not active" };
|
|
117
194
|
}
|
|
118
|
-
if (action === "run")
|
|
195
|
+
if (action === "run")
|
|
196
|
+
return this.run(params.request, ctx, signal, observer);
|
|
119
197
|
if (action === "apply") {
|
|
120
198
|
if (!ctx || !params.resultId)
|
|
121
199
|
return { ok: false, error: "apply requires context and resultId" };
|
|
@@ -128,12 +206,11 @@ export class Runtime {
|
|
|
128
206
|
? { ok: true, action }
|
|
129
207
|
: { ok: false, error: "retained result not found" };
|
|
130
208
|
}
|
|
131
|
-
if (action === "cancel")
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
});
|
|
209
|
+
if (action === "cancel") {
|
|
210
|
+
await this.scheduler.cancelAll();
|
|
211
|
+
return { ok: true, action };
|
|
212
|
+
}
|
|
213
|
+
await this.drain();
|
|
137
214
|
return { ok: true, action: "finish" };
|
|
138
215
|
}
|
|
139
216
|
|
|
@@ -160,13 +237,30 @@ export class Runtime {
|
|
|
160
237
|
agent: { role: string; content: string },
|
|
161
238
|
envelope: RequestEnvelope,
|
|
162
239
|
ctx: RunContext,
|
|
163
|
-
|
|
240
|
+
signal: AbortSignal,
|
|
241
|
+
): Promise<DispatchResult & { failureKind?: RunFailureKind }> {
|
|
242
|
+
if (signal.aborted)
|
|
243
|
+
return {
|
|
244
|
+
...cancellationError(signal),
|
|
245
|
+
failureKind: "cancelled",
|
|
246
|
+
};
|
|
164
247
|
let phase: Awaited<ReturnType<typeof runtimeFromContext>>;
|
|
165
248
|
try {
|
|
166
|
-
phase = await runtimeFromContext(ctx);
|
|
249
|
+
phase = await runtimeFromContext(ctx, signal);
|
|
167
250
|
} catch (error) {
|
|
168
|
-
return {
|
|
251
|
+
return {
|
|
252
|
+
ok: false,
|
|
253
|
+
error: signal.aborted
|
|
254
|
+
? cancellationError(signal).error
|
|
255
|
+
: (error as Error).message,
|
|
256
|
+
failureKind: signal.aborted ? "cancelled" : "failed",
|
|
257
|
+
};
|
|
169
258
|
}
|
|
259
|
+
if (signal.aborted)
|
|
260
|
+
return {
|
|
261
|
+
...cancellationError(signal),
|
|
262
|
+
failureKind: "cancelled",
|
|
263
|
+
};
|
|
170
264
|
const roots = envelope.roots.map(
|
|
171
265
|
(root) => new URL(root, `file://${ctx.cwd}/`).pathname,
|
|
172
266
|
);
|
|
@@ -187,8 +281,14 @@ export class Runtime {
|
|
|
187
281
|
output: envelope.output as "evidence" | "diff",
|
|
188
282
|
roots,
|
|
189
283
|
timeoutMs: LIMITS.phaseTimeoutMs,
|
|
284
|
+
signal,
|
|
190
285
|
});
|
|
191
|
-
if (
|
|
286
|
+
if (signal.aborted)
|
|
287
|
+
return {
|
|
288
|
+
...cancellationError(signal),
|
|
289
|
+
failureKind: "cancelled",
|
|
290
|
+
};
|
|
291
|
+
if (!child.ok) return { ...child, failureKind: child.failureKind };
|
|
192
292
|
if (envelope.output === "evidence") {
|
|
193
293
|
return {
|
|
194
294
|
ok: true,
|
|
@@ -221,17 +321,140 @@ export class Runtime {
|
|
|
221
321
|
private async run(
|
|
222
322
|
request: unknown,
|
|
223
323
|
ctx?: RunContext,
|
|
324
|
+
signal?: AbortSignal,
|
|
325
|
+
observer?: RuntimeActivityObserver,
|
|
224
326
|
): Promise<DispatchResult> {
|
|
225
327
|
if (!ctx) return { ok: false, error: "run requires extension context" };
|
|
328
|
+
if (signal?.aborted) return cancellationError(signal);
|
|
226
329
|
const validation = validateRequestEnvelope(request);
|
|
227
330
|
if (!validation.ok) return { ok: false, error: validation.reason };
|
|
228
|
-
const envelope =
|
|
331
|
+
const envelope: RequestEnvelope = {
|
|
332
|
+
...validation.value,
|
|
333
|
+
roots: [...validation.value.roots],
|
|
334
|
+
context: { ...validation.value.context },
|
|
335
|
+
declared: {
|
|
336
|
+
...validation.value.declared,
|
|
337
|
+
read: [...validation.value.declared.read],
|
|
338
|
+
write: [...validation.value.declared.write],
|
|
339
|
+
conflicts: [...validation.value.declared.conflicts],
|
|
340
|
+
resources: [...validation.value.declared.resources],
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
const context: StoredRunContext = {
|
|
344
|
+
ctx,
|
|
345
|
+
observer,
|
|
346
|
+
sequence: ++this.batchSeq,
|
|
347
|
+
runningEmitted: false,
|
|
348
|
+
terminalEmitted: false,
|
|
349
|
+
};
|
|
350
|
+
this.runContexts.set(envelope, context);
|
|
351
|
+
this.notify(envelope, context, "queued");
|
|
352
|
+
const batchId = `runtime-${context.sequence}`;
|
|
353
|
+
const batch = this.scheduler.schedule(batchId, [
|
|
354
|
+
{ request: envelope, prerequisites: [] },
|
|
355
|
+
]);
|
|
356
|
+
const cancelBatch = () =>
|
|
357
|
+
this.scheduler.cancel(
|
|
358
|
+
batchId,
|
|
359
|
+
signal?.reason ?? new Error("tool call cancelled"),
|
|
360
|
+
);
|
|
361
|
+
if (signal?.aborted) cancelBatch();
|
|
362
|
+
else signal?.addEventListener("abort", cancelBatch, { once: true });
|
|
363
|
+
try {
|
|
364
|
+
const outcome = await batch.result(envelope.id);
|
|
365
|
+
if (outcome.status === "succeeded" && outcome.value) {
|
|
366
|
+
const internalResult = outcome.value.dispatch as DispatchResult & {
|
|
367
|
+
failureKind?: RunFailureKind;
|
|
368
|
+
};
|
|
369
|
+
const { failureKind, ...publicResult } = internalResult;
|
|
370
|
+
if (publicResult.ok) {
|
|
371
|
+
this.notify(envelope, context, "completed");
|
|
372
|
+
return publicResult;
|
|
373
|
+
}
|
|
374
|
+
const state: RuntimeActivityState = failureKind ?? "failed";
|
|
375
|
+
this.notify(envelope, context, state);
|
|
376
|
+
return publicResult;
|
|
377
|
+
}
|
|
378
|
+
const state: RuntimeActivityState =
|
|
379
|
+
outcome.status === "cancelled" ? "cancelled" : "failed";
|
|
380
|
+
const terminal = {
|
|
381
|
+
ok: false as const,
|
|
382
|
+
error: outcome.error ?? `scheduled run ${outcome.status}`,
|
|
383
|
+
};
|
|
384
|
+
this.notify(envelope, context, state);
|
|
385
|
+
return terminal;
|
|
386
|
+
} finally {
|
|
387
|
+
signal?.removeEventListener("abort", cancelBatch);
|
|
388
|
+
this.runContexts.delete(envelope);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private notify(
|
|
393
|
+
envelope: RequestEnvelope,
|
|
394
|
+
context: StoredRunContext,
|
|
395
|
+
state: RuntimeActivityState,
|
|
396
|
+
): void {
|
|
397
|
+
if (!context.observer || context.terminalEmitted) return;
|
|
398
|
+
if (state === "running") {
|
|
399
|
+
if (context.runningEmitted) return;
|
|
400
|
+
context.runningEmitted = true;
|
|
401
|
+
}
|
|
402
|
+
if (
|
|
403
|
+
state === "completed" ||
|
|
404
|
+
state === "failed" ||
|
|
405
|
+
state === "cancelled" ||
|
|
406
|
+
state === "timed-out"
|
|
407
|
+
) {
|
|
408
|
+
context.terminalEmitted = true;
|
|
409
|
+
}
|
|
410
|
+
const event: RuntimeActivityEvent = {
|
|
411
|
+
state,
|
|
412
|
+
requestId: envelope.id,
|
|
413
|
+
role: envelope.role,
|
|
414
|
+
phase: envelope.phase,
|
|
415
|
+
objective: envelope.objective,
|
|
416
|
+
sequence: context.sequence,
|
|
417
|
+
...(state === "cancelled"
|
|
418
|
+
? { failureReason: "subagent cancelled" as const }
|
|
419
|
+
: state === "timed-out"
|
|
420
|
+
? { failureReason: "phase timed out" as const }
|
|
421
|
+
: state === "failed"
|
|
422
|
+
? { failureReason: "subagent failed" as const }
|
|
423
|
+
: {}),
|
|
424
|
+
};
|
|
425
|
+
try {
|
|
426
|
+
const result = context.observer(event);
|
|
427
|
+
if (result instanceof Promise) void result.catch(() => undefined);
|
|
428
|
+
} catch {
|
|
429
|
+
// Presentation observers cannot affect orchestration.
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private async runScheduled(
|
|
434
|
+
envelope: RequestEnvelope,
|
|
435
|
+
ctx: RunContext,
|
|
436
|
+
signal: AbortSignal,
|
|
437
|
+
): Promise<DispatchResult & { failureKind?: RunFailureKind }> {
|
|
438
|
+
if (signal.aborted)
|
|
439
|
+
return {
|
|
440
|
+
...cancellationError(signal),
|
|
441
|
+
failureKind: "cancelled",
|
|
442
|
+
};
|
|
229
443
|
const agent = loadAgentDefinitions().find(
|
|
230
444
|
(item) => item.role === envelope.role,
|
|
231
445
|
);
|
|
232
|
-
if (!agent)
|
|
446
|
+
if (!agent)
|
|
447
|
+
return {
|
|
448
|
+
ok: false,
|
|
449
|
+
error: "package Agent role not found",
|
|
450
|
+
failureKind: "failed",
|
|
451
|
+
};
|
|
233
452
|
if (!ctx.model)
|
|
234
|
-
return {
|
|
453
|
+
return {
|
|
454
|
+
ok: false,
|
|
455
|
+
error: "run requires a model identity",
|
|
456
|
+
failureKind: "failed",
|
|
457
|
+
};
|
|
235
458
|
const identity = workerIdentity(ctx.model);
|
|
236
459
|
const contract = contractOf(envelope);
|
|
237
460
|
const existing = this.registry.get(envelope.id);
|
|
@@ -241,6 +464,7 @@ export class Runtime {
|
|
|
241
464
|
ok: false,
|
|
242
465
|
error:
|
|
243
466
|
"blocked: logical Worker pinned provider/model identity differs",
|
|
467
|
+
failureKind: "failed",
|
|
244
468
|
};
|
|
245
469
|
}
|
|
246
470
|
if (!sameContract(existing.contract, contract)) {
|
|
@@ -248,19 +472,26 @@ export class Runtime {
|
|
|
248
472
|
ok: false,
|
|
249
473
|
error:
|
|
250
474
|
"blocked: logical Worker contract changed for a recovery scope",
|
|
475
|
+
failureKind: "failed",
|
|
251
476
|
};
|
|
252
477
|
}
|
|
253
478
|
if (existing.redispatchUsed) {
|
|
254
479
|
return {
|
|
255
480
|
ok: false,
|
|
256
481
|
error: "blocked: mechanical redispatch is already exhausted",
|
|
482
|
+
failureKind: "failed",
|
|
257
483
|
};
|
|
258
484
|
}
|
|
259
485
|
} else {
|
|
260
486
|
this.registry.pin(contract, identity);
|
|
261
487
|
}
|
|
262
488
|
const pinned = this.registry.get(envelope.id);
|
|
263
|
-
const first = await this.dispatchChild(agent, envelope, ctx);
|
|
489
|
+
const first = await this.dispatchChild(agent, envelope, ctx, signal);
|
|
490
|
+
if (signal.aborted)
|
|
491
|
+
return {
|
|
492
|
+
...cancellationError(signal),
|
|
493
|
+
failureKind: "cancelled",
|
|
494
|
+
};
|
|
264
495
|
if (first.ok) {
|
|
265
496
|
if (pinned) pinned.redispatchUsed = false;
|
|
266
497
|
return first;
|
|
@@ -269,17 +500,28 @@ export class Runtime {
|
|
|
269
500
|
return {
|
|
270
501
|
ok: false,
|
|
271
502
|
error: "blocked: mechanical redispatch failed and is exhausted",
|
|
503
|
+
failureKind: first.failureKind ?? "failed",
|
|
272
504
|
};
|
|
273
505
|
}
|
|
274
506
|
if (!pinned) {
|
|
275
|
-
return {
|
|
507
|
+
return {
|
|
508
|
+
ok: false,
|
|
509
|
+
error: "blocked: logical Worker is not pinned",
|
|
510
|
+
failureKind: "failed",
|
|
511
|
+
};
|
|
276
512
|
}
|
|
277
513
|
pinned.redispatchUsed = true;
|
|
278
|
-
const second = await this.dispatchChild(agent, envelope, ctx);
|
|
514
|
+
const second = await this.dispatchChild(agent, envelope, ctx, signal);
|
|
515
|
+
if (signal.aborted)
|
|
516
|
+
return {
|
|
517
|
+
...cancellationError(signal),
|
|
518
|
+
failureKind: "cancelled",
|
|
519
|
+
};
|
|
279
520
|
if (!second.ok) {
|
|
280
521
|
return {
|
|
281
522
|
ok: false,
|
|
282
523
|
error: "blocked: identical mechanical redispatch failed again",
|
|
524
|
+
failureKind: second.failureKind ?? first.failureKind ?? "failed",
|
|
283
525
|
};
|
|
284
526
|
}
|
|
285
527
|
return second;
|
|
@@ -291,12 +533,14 @@ export class Runtime {
|
|
|
291
533
|
return validateRequestEnvelope(envelope);
|
|
292
534
|
}
|
|
293
535
|
|
|
294
|
-
drain(): void {
|
|
536
|
+
async drain(): Promise<void> {
|
|
537
|
+
const settled = this.scheduler.cancelAll();
|
|
295
538
|
drainStage({
|
|
296
539
|
results: this.results,
|
|
297
540
|
registry: this.registry,
|
|
298
541
|
activation: this.activation,
|
|
299
542
|
});
|
|
543
|
+
await settled;
|
|
300
544
|
}
|
|
301
545
|
|
|
302
546
|
get state(): ActivationState {
|
package/src/scheduler.ts
CHANGED
|
@@ -43,6 +43,7 @@ interface Entry<T> {
|
|
|
43
43
|
state: EntryState;
|
|
44
44
|
attempts: number;
|
|
45
45
|
cancelRequested: boolean;
|
|
46
|
+
cancelReason: unknown;
|
|
46
47
|
controller: AbortController | null;
|
|
47
48
|
resolve(outcome: ScheduledOutcome<T>): void;
|
|
48
49
|
promise: Promise<ScheduledOutcome<T>>;
|
|
@@ -106,6 +107,7 @@ class Batch<T> {
|
|
|
106
107
|
state: "queued",
|
|
107
108
|
attempts: 0,
|
|
108
109
|
cancelRequested: false,
|
|
110
|
+
cancelReason: undefined,
|
|
109
111
|
controller: null,
|
|
110
112
|
resolve,
|
|
111
113
|
promise,
|
|
@@ -150,6 +152,8 @@ export class Scheduler<T> {
|
|
|
150
152
|
}
|
|
151
153
|
}
|
|
152
154
|
this.pump();
|
|
155
|
+
const done = Promise.all(batch.entries.map((entry) => entry.promise));
|
|
156
|
+
void done.then(() => this.removeBatch(batch));
|
|
153
157
|
return {
|
|
154
158
|
batchId,
|
|
155
159
|
result: (id: string) => {
|
|
@@ -157,22 +161,54 @@ export class Scheduler<T> {
|
|
|
157
161
|
if (!entry) throw new Error(`unknown scheduled request: ${id}`);
|
|
158
162
|
return entry.promise;
|
|
159
163
|
},
|
|
160
|
-
done
|
|
164
|
+
done,
|
|
161
165
|
};
|
|
162
166
|
}
|
|
163
167
|
|
|
164
|
-
|
|
168
|
+
private removeBatch(batch: Batch<T>): void {
|
|
169
|
+
const index = this.batches.indexOf(batch);
|
|
170
|
+
if (index >= 0) this.batches.splice(index, 1);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
cancel(
|
|
174
|
+
batchId: string,
|
|
175
|
+
reason: unknown = new Error("batch cancelled"),
|
|
176
|
+
): void {
|
|
165
177
|
const batch = this.batches.find(
|
|
166
178
|
(candidate) => candidate.batchId === batchId,
|
|
167
179
|
);
|
|
168
|
-
if (
|
|
180
|
+
if (batch) this.cancelBatch(batch, reason);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async cancelAll(
|
|
184
|
+
reason: unknown = new Error("batch cancelled"),
|
|
185
|
+
): Promise<void> {
|
|
186
|
+
const batches = [...this.batches];
|
|
187
|
+
for (const batch of batches) this.requestCancellation(batch, reason);
|
|
188
|
+
for (const batch of batches) this.settleQueuedCancellation(batch);
|
|
189
|
+
await Promise.all(
|
|
190
|
+
batches.flatMap((batch) => batch.entries.map((entry) => entry.promise)),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private cancelBatch(batch: Batch<T>, reason: unknown): void {
|
|
195
|
+
this.requestCancellation(batch, reason);
|
|
196
|
+
this.settleQueuedCancellation(batch);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private requestCancellation(batch: Batch<T>, reason: unknown): void {
|
|
200
|
+
for (const entry of batch.entries) {
|
|
201
|
+
if (entry.state !== "queued" && entry.state !== "running") continue;
|
|
202
|
+
if (!entry.cancelRequested) entry.cancelReason = reason;
|
|
203
|
+
entry.cancelRequested = true;
|
|
204
|
+
if (entry.state === "running") entry.controller?.abort(reason);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private settleQueuedCancellation(batch: Batch<T>): void {
|
|
169
209
|
for (const entry of batch.entries) {
|
|
170
210
|
if (entry.state === "queued") {
|
|
171
|
-
entry.cancelRequested = true;
|
|
172
211
|
this.settle(batch, entry, this.cancelOutcome(entry));
|
|
173
|
-
} else if (entry.state === "running") {
|
|
174
|
-
entry.cancelRequested = true;
|
|
175
|
-
entry.controller?.abort();
|
|
176
212
|
}
|
|
177
213
|
}
|
|
178
214
|
this.pump();
|
|
@@ -182,7 +218,7 @@ export class Scheduler<T> {
|
|
|
182
218
|
for (const batch of this.batches) {
|
|
183
219
|
for (const entry of batch.entries) {
|
|
184
220
|
if (this.active.size >= this.limit) return;
|
|
185
|
-
if (entry.state !== "queued") continue;
|
|
221
|
+
if (entry.state !== "queued" || entry.cancelRequested) continue;
|
|
186
222
|
if (!this.prerequisitesSucceeded(batch, entry)) continue;
|
|
187
223
|
if (this.hasActiveConflict(entry)) continue;
|
|
188
224
|
this.start(batch, entry);
|
|
@@ -207,7 +243,7 @@ export class Scheduler<T> {
|
|
|
207
243
|
|
|
208
244
|
private start(batch: Batch<T>, entry: Entry<T>): void {
|
|
209
245
|
// Guarded start: a cancelled or settled queued closure never runs later.
|
|
210
|
-
if (entry.state !== "queued") return;
|
|
246
|
+
if (entry.state !== "queued" || entry.cancelRequested) return;
|
|
211
247
|
entry.state = "running";
|
|
212
248
|
entry.attempts += 1;
|
|
213
249
|
entry.controller = new AbortController();
|
|
@@ -262,7 +298,7 @@ export class Scheduler<T> {
|
|
|
262
298
|
id: entry.request.id,
|
|
263
299
|
status: "cancelled",
|
|
264
300
|
attempts: entry.attempts,
|
|
265
|
-
error: "batch cancelled",
|
|
301
|
+
error: errorMessage(entry.cancelReason ?? new Error("batch cancelled")),
|
|
266
302
|
};
|
|
267
303
|
}
|
|
268
304
|
|