@vzn/vx-dte 0.0.652 → 0.0.653

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/src/planner.ts CHANGED
@@ -18,6 +18,9 @@ import {
18
18
  } from './dispatch.js'
19
19
  import { DEFAULT_STRATEGY, type Strategy } from './strategy.js'
20
20
 
21
+ /** Where a task naming no platform runs, or (several worker types) where each task runs. */
22
+ type Home = string | ((node: TaskNode) => string | undefined)
23
+
21
24
  /**
22
25
  * A task's duration when history has none: a test suite's, so a run with
23
26
  * no history starts workers for its work rather than one for all of it.
@@ -53,6 +56,8 @@ export interface PlanTask {
53
56
  /** Its number in the run's graph and its deps' (`planTasks`): a plan's shape reads them, not ids. */
54
57
  readonly n?: number
55
58
  readonly depsN?: Int32Array
59
+ /** Its servers' bases (`Need.servers`): booked once on each worker. */
60
+ readonly servers?: Need['servers']
56
61
  }
57
62
 
58
63
  /** The run's tasks numbered once, and each one's deps by number. */
@@ -97,6 +102,8 @@ export interface PlanWorker {
97
102
  readonly platform?: string | undefined
98
103
  /** The size it was started as (`Size.name`): what its warm-up is learned by. */
99
104
  readonly size?: string | undefined
105
+ /** The servers it runs, each with the base booked there. */
106
+ readonly serving?: ReadonlyMap<string, number> | undefined
100
107
  }
101
108
 
102
109
  /** A worker's warm-up, one for all, or by its size (`Size.name`; none for one that joined on its own). */
@@ -131,14 +138,21 @@ interface Ahead {
131
138
  /** When the window it played moves on, from now: plan again then. */
132
139
  readonly untilInMs: number | undefined
133
140
  readonly freeInMs: ReadonlyMap<string, number>
141
+ /** The tasks it gives each worker. */
142
+ readonly given: ReadonlyMap<string, readonly string[]>
134
143
  readonly later: readonly {
135
144
  readonly size: Size
136
145
  readonly inMs: number
137
146
  readonly freeInMs: number
138
147
  readonly joinInMs: number
148
+ readonly given: readonly string[]
149
+ }[]
150
+ /** For each started now, when its work ends, when it joins and what it runs. */
151
+ readonly started: readonly {
152
+ readonly freeInMs: number
153
+ readonly joinInMs: number
154
+ readonly given: readonly string[]
139
155
  }[]
140
- /** For each started now, when its work ends and when it joins. */
141
- readonly started: readonly { readonly freeInMs: number; readonly joinInMs: number }[]
142
156
  }
143
157
 
144
158
  /** History the run's finished tasks must have had before `slowdown` says anything. */
@@ -202,7 +216,7 @@ export function planTasks(
202
216
  nodes: ReadonlyMap<string, TaskNode>,
203
217
  need: (id: string) => Need,
204
218
  durationMs: (id: string) => number | undefined,
205
- home?: string,
219
+ home?: Home,
206
220
  ): PlanTask[] {
207
221
  // The graph is the run's, fixed: each task is numbered and its deps
208
222
  // walked once a run, not at each plan (2026-11-05: a 5000-task sim
@@ -247,9 +261,10 @@ export function planTasks(
247
261
  above: n.above,
248
262
  ms: durationMs(id),
249
263
  deps,
250
- platform: e.node.platform ?? home,
264
+ platform: typeof home === 'function' ? home(e.node) : (e.node.platform ?? home),
251
265
  n: e.n,
252
266
  depsN: e.depsN,
267
+ ...(n.servers === undefined ? {} : { servers: n.servers }),
253
268
  })
254
269
  }
255
270
  return out
@@ -272,7 +287,7 @@ export function missedWork(
272
287
  executed: ReadonlySet<string>,
273
288
  need: (id: string) => Need,
274
289
  durationMs: (id: string) => number | undefined,
275
- home?: string,
290
+ home?: Home,
276
291
  ): PlanTask[] {
277
292
  const changed = new Map<string, boolean>()
278
293
  // Whether `id`'s key changed: a cached task that missed, or one after it.
@@ -294,6 +309,43 @@ export function missedWork(
294
309
  return planTasks([...remaining].filter(missed), nodes, need, durationMs, home)
295
310
  }
296
311
 
312
+ /**
313
+ * `workers` as a plan may let them go: of a platform whose sizes can start
314
+ * no other (`max` spent; one let go still counts), the last idle one stays
315
+ * while a task of that platform has not ended (`left`). A hit core's probe
316
+ * found can still miss when its restore fails, and only a worker of its
317
+ * platform runs it: main let its one arm worker go, check.binary.linux-arm64
318
+ * came back as a miss, and the run waited out its timeout (2026-11-06).
319
+ */
320
+ export function keepLast(
321
+ workers: readonly PlanWorker[],
322
+ sizes: readonly Size[],
323
+ started: ReadonlyMap<string, number>,
324
+ left: (platform: string) => boolean,
325
+ ): readonly PlanWorker[] {
326
+ const of = new Map<string, PlanWorker[]>()
327
+ for (const w of workers) {
328
+ if (w.platform === undefined) continue
329
+ const all = of.get(w.platform)
330
+ if (all === undefined) of.set(w.platform, [w])
331
+ else all.push(w)
332
+ }
333
+ let kept: Set<PlanWorker> | undefined
334
+ for (const [platform, all] of of) {
335
+ // A busy one is not let go; one starting, cancelled, frees its start.
336
+ if (all.some((w) => !w.idle && w.starting !== true)) continue
337
+ const idle = all.find((w) => w.idle && w.starting !== true)
338
+ if (idle === undefined) continue
339
+ const own = sizes.filter((s) => s.platform === platform)
340
+ if (own.length === 0 || own.some((s) => (started.get(s.name) ?? 0) < s.max)) continue
341
+ if (!left(platform)) continue
342
+ ;(kept ??= new Set()).add(idle)
343
+ }
344
+ return kept === undefined
345
+ ? workers
346
+ : workers.map((w) => (kept!.has(w) ? { ...w, idle: false } : w))
347
+ }
348
+
297
349
  /**
298
350
  * The tasks a plan counts as missed: those handed over to run, the
299
351
  * misses core's probe found, and of the commit's forecast (`likely`) only
@@ -319,6 +371,8 @@ interface Slot extends Bin {
319
371
  free?: number
320
372
  /** When its last task given in the play ends. */
321
373
  last?: number
374
+ /** The tasks given it in the play. */
375
+ given?: string[]
322
376
  }
323
377
 
324
378
  /**
@@ -350,6 +404,7 @@ function play(
350
404
  first: Map<string, number>
351
405
  ends: Map<string | undefined, number>
352
406
  free: () => Map<string, number>
407
+ given: () => Map<string, readonly string[]>
353
408
  } {
354
409
  // A dependency running now ends when it does; one ended or a hit, now.
355
410
  const runningIds = new Set<string>()
@@ -366,8 +421,9 @@ function play(
366
421
  memory: w.memory,
367
422
  platform: w.platform,
368
423
  at: w.at,
369
- used: { cores: 0, memory: 0 },
424
+ used: { cores: [...(w.serving?.values() ?? [])].reduce((a, b) => a + b, 0), memory: 0 },
370
425
  running: 0,
426
+ serving: new Set(w.serving?.keys()),
371
427
  }))
372
428
  // Ends to come: [at, slot, cores, memory, task or undefined].
373
429
  const ends = new Ends()
@@ -479,6 +535,7 @@ function play(
479
535
  readyCount--
480
536
  count(t, -1)
481
537
  best.first ??= now
538
+ ;(best.given ??= []).push(id)
482
539
  best.free = Math.max(best.free ?? 0, now + (t.ms ?? UNKNOWN_MS))
483
540
  const end = now + (t.ms ?? UNKNOWN_MS)
484
541
  ends.push(
@@ -497,8 +554,10 @@ function play(
497
554
  new Map(slots.flatMap((s) => (s.first === undefined ? [] : [[s.id, s.first] as const])))
498
555
  const free = (): Map<string, number> =>
499
556
  new Map(slots.flatMap((s) => (s.free === undefined ? [] : [[s.id, s.free] as const])))
557
+ const given = (): Map<string, readonly string[]> =>
558
+ new Map(slots.flatMap((s) => (s.given === undefined ? [] : [[s.id, s.given] as const])))
500
559
  if (left === 0 && readyCount === 0)
501
- return { end: last, first: first(), ends: byPlatform(), free }
560
+ return { end: last, first: first(), ends: byPlatform(), free, given }
502
561
  // The next moment anything changes: a task ends or a worker joins.
503
562
  let next = ends.next()
504
563
  for (const s of slots) if (s.at > now) next = Math.min(next, s.at)
@@ -512,18 +571,24 @@ function play(
512
571
  cores.set(undefined, (cores.get(undefined) ?? 0) + s.cores)
513
572
  if (s.platform !== undefined) cores.set(s.platform, (cores.get(s.platform) ?? 0) + s.cores)
514
573
  }
515
- for (const e of ends.all())
516
- work.set(undefined, (work.get(undefined) ?? 0) + e.cores * (e.at - now))
574
+ // A running task's rest on its own platform too: counted on all
575
+ // alone, a Mac fewer read the Linux end later (2026-11-07).
576
+ for (const e of ends.all()) {
577
+ const w = e.cores * (e.at - now)
578
+ work.set(undefined, (work.get(undefined) ?? 0) + w)
579
+ const p = e.s.platform
580
+ if (p !== undefined) work.set(p, (work.get(p) ?? 0) + w)
581
+ }
517
582
  let end = last
518
583
  for (const [p, w] of work) end = Math.max(end, now + w / Math.max(cores.get(p) ?? 0, 1e-9))
519
584
  for (const t of ready.live()) end = Math.max(end, now + tailOf(t.id))
520
585
  for (const e of ends.all())
521
586
  for (const d of (e.place >= 0 ? afterAt(e.place) : after.get(e.id!)) ?? [])
522
587
  end = Math.max(end, e.at + tailAt(d))
523
- return { end, cut: true, first: first(), ends: byPlatform(), free }
588
+ return { end, cut: true, first: first(), ends: byPlatform(), free, given }
524
589
  }
525
590
  if (next === Infinity)
526
- return { end: left === 0 ? last : Infinity, first: first(), ends: byPlatform(), free }
591
+ return { end: left === 0 ? last : Infinity, first: first(), ends: byPlatform(), free, given }
527
592
  now = next
528
593
  while (ends.next() <= now) {
529
594
  const { s, cores, memory, place, id } = ends.pop()
@@ -782,6 +847,55 @@ function chainMs(tasks: readonly PlanTask[], workers: readonly PlanWorker[]): nu
782
847
  const twins = (a: PlanWorker, b: PlanWorker): boolean =>
783
848
  a.cores === b.cores && a.memory === b.memory && a.platform === b.platform && a.at === b.at
784
849
 
850
+ type Played = ReturnType<typeof play>
851
+
852
+ /**
853
+ * `kept` less each of `added` (its last ones, in the order the run would
854
+ * start them) the run ends as soon without, tried last first. Sizes come in
855
+ * turns, so a prefix holds ones it ends as soon without: a second Mac the
856
+ * plan needed came after a second Linux worker it did not, and main e118ccf
857
+ * cancelled that one unjoined (2026-10-24). Dropped when the end moves no
858
+ * later and its platform's moves no further than the margin, or still ends
859
+ * two warm-ups before the run: room for runners the CI queues. To the run's
860
+ * end alone, a 110 s guess of an unmeasured chain cut every other platform
861
+ * to it; to its platform's alone, 4 Macs started for work 2 ended 40 s
862
+ * before the Linux work (main a1950a2, 2026-11-07: "we optimize for total
863
+ * wall time"). The live plan and the footer's advice both trim by it.
864
+ */
865
+ function dropSpare(
866
+ added: readonly PlanWorker[],
867
+ kept: readonly PlanWorker[],
868
+ before: Played,
869
+ run: (ws: readonly PlanWorker[]) => Played,
870
+ ): { kept: PlanWorker[]; before: Played } {
871
+ let out = [...kept]
872
+ for (let i = added.length - 1, dropped = false; i >= 0; i--) {
873
+ const w = added[i]!
874
+ // Its twin after it stayed, and nothing moved since: without this
875
+ // one the play is that one's again, so it stays too. Each was a
876
+ // play of the whole run (2026-11-05).
877
+ if (i < added.length - 1 && !dropped && twins(w, added[i + 1]!)) continue
878
+ const without = out.filter((k) => k !== w)
879
+ // Given no task in a whole play, the play without it is the same.
880
+ if (!before.first.has(w.id) && before.cut !== true) {
881
+ out = without
882
+ dropped = true
883
+ continue
884
+ }
885
+ const after = run(without)
886
+ const ends = after.ends.get(w.platform) ?? 0
887
+ dropped =
888
+ after.end <= before.end &&
889
+ (ends <= (before.ends.get(w.platform) ?? 0) + MARGIN_MS ||
890
+ ends <= before.end - Math.max(MARGIN_MS, 2 * w.at))
891
+ if (dropped) {
892
+ out = without
893
+ before = after
894
+ }
895
+ }
896
+ return { kept: out, before }
897
+ }
898
+
785
899
  /** A worker of `s` that joins `at`. */
786
900
  function fresh(
787
901
  s: { cpu: number; memory: number; platform?: string | undefined; size?: string | undefined },
@@ -875,36 +989,9 @@ export function plan(
875
989
  }
876
990
  const n = lo
877
991
  let kept = [...workers, ...added.slice(0, n)]
878
- // Sizes come in turns, so the prefix can hold one the plan ends as soon
879
- // without: a second Mac the plan needed came after a second Linux worker
880
- // it did not, and main e118ccf cancelled that one unjoined (2026-10-24).
881
- // Dropped only when neither the end nor its platform's moves past the
882
- // margin: to the run's end alone, a 110 s guess of an unmeasured chain
883
- // cut every other platform to it.
992
+ // The ones the run ends as soon without go (`dropSpare`).
884
993
  let before = best < Infinity && n > 0 ? playOf(n) : undefined
885
- if (before !== undefined)
886
- for (let i = n - 1, dropped = false; i >= 0; i--) {
887
- // Its twin after it stayed, and nothing moved since: without this
888
- // one the play is that one's again, so it stays too. Each was a
889
- // play of the whole run (2026-11-05).
890
- if (i < n - 1 && !dropped && twins(added[i]!, added[i + 1]!)) continue
891
- const platform = added[i]!.platform
892
- const without = kept.filter((k) => k !== added[i])
893
- // Given no task in a whole play, the play without it is the same.
894
- if (unused(before, added[i]!.id)) {
895
- kept = without
896
- dropped = true
897
- continue
898
- }
899
- const after = run(without)
900
- dropped =
901
- after.end <= before.end &&
902
- (after.ends.get(platform) ?? 0) <= (before.ends.get(platform) ?? 0) + MARGIN_MS
903
- if (dropped) {
904
- kept = without
905
- before = after
906
- }
907
- }
994
+ if (before !== undefined) ({ kept, before } = dropSpare(added.slice(0, n), kept, before, run))
908
995
  const release: string[] = []
909
996
  // Played with the rest: left out, the next idle one and the plan's end
910
997
  // counted on a worker the run lets go.
@@ -977,11 +1064,12 @@ export function plan(
977
1064
  }
978
1065
  }
979
1066
  }
980
- const { end, first, free, cut } = run([...kept, ...standIns])
1067
+ const { end, first, free, cut, given } = run([...kept, ...standIns])
981
1068
  const start: Size[] = []
982
1069
  const frees = free()
983
- const started: { freeInMs: number; joinInMs: number }[] = []
984
- const later: { size: Size; inMs: number; freeInMs: number; joinInMs: number }[] = []
1070
+ const gave = given()
1071
+ const started: Ahead['started'][number][] = []
1072
+ const later: Ahead['later'][number][] = []
985
1073
  let againInMs: number | undefined
986
1074
  for (let i = 0; i < n; i++) {
987
1075
  const id = added[i]!.id
@@ -993,11 +1081,17 @@ export function plan(
993
1081
  // same plan 100,000 times at one instant (tests/sim.test.ts).
994
1082
  if (at < warmUp + MARGIN_MS + 1) {
995
1083
  start.push(candidates[i]!)
996
- started.push({ freeInMs: frees.get(id)!, joinInMs: warmUp })
1084
+ started.push({ freeInMs: frees.get(id)!, joinInMs: warmUp, given: gave.get(id)! })
997
1085
  } else {
998
1086
  const inMs = at - warmUp - MARGIN_MS
999
1087
  againInMs = Math.min(againInMs ?? Infinity, inMs)
1000
- later.push({ size: candidates[i]!, inMs, freeInMs: frees.get(id)!, joinInMs: warmUp })
1088
+ later.push({
1089
+ size: candidates[i]!,
1090
+ inMs,
1091
+ freeInMs: frees.get(id)!,
1092
+ joinInMs: warmUp,
1093
+ given: gave.get(id)!,
1094
+ })
1001
1095
  }
1002
1096
  }
1003
1097
  later.sort((a, b) => a.inMs - b.inMs)
@@ -1011,6 +1105,7 @@ export function plan(
1011
1105
  againInMs: untilInMs === undefined ? againInMs : Math.min(againInMs ?? Infinity, untilInMs),
1012
1106
  untilInMs,
1013
1107
  freeInMs: frees,
1108
+ given: gave,
1014
1109
  later,
1015
1110
  started,
1016
1111
  }
@@ -1090,8 +1185,8 @@ export function decide(input: {
1090
1185
  readonly known?: ReadonlySet<string>
1091
1186
  /** How it orders, places and scales (`need` books already). */
1092
1187
  readonly how?: Strategy
1093
- /** The platform a task naming none runs on (`homeOf`). */
1094
- readonly home?: string | undefined
1188
+ /** The platform a task naming none runs on (`homeOf`), or where each task runs (several worker types). */
1189
+ readonly home?: Home | undefined
1095
1190
  /**
1096
1191
  * Sizes with a worker asked and not joined past its warm-up: the CI
1097
1192
  * queued it, and one asked now queues behind it. None of them starts.
@@ -1152,7 +1247,7 @@ export function decide(input: {
1152
1247
  return { ...backlog(tasks, input.workers, sizes, input.warmUpMs, how), tasks }
1153
1248
  const step = had === undefined ? undefined : follow(had, tasks, input, sizes, now)
1154
1249
  if (step !== undefined && step !== 'full') return shown(input.last!, had!, step, now, tasks)
1155
- const { freeInMs, later, started, untilInMs, ...p } = plan(
1250
+ const { freeInMs, given, later, started, untilInMs, ...p } = plan(
1156
1251
  tasks,
1157
1252
  input.workers,
1158
1253
  sizes,
@@ -1163,7 +1258,13 @@ export function decide(input: {
1163
1258
  how,
1164
1259
  )
1165
1260
  if (input.last !== undefined)
1166
- input.last.forecast = forecastOf(p, { freeInMs, later, started, untilInMs }, tasks, input, now)
1261
+ input.last.forecast = forecastOf(
1262
+ p,
1263
+ { freeInMs, given, later, started, untilInMs },
1264
+ tasks,
1265
+ input,
1266
+ now,
1267
+ )
1167
1268
  return { ...p, tasks }
1168
1269
  }
1169
1270
 
@@ -1186,16 +1287,23 @@ export interface Forecast {
1186
1287
  readonly count: number
1187
1288
  /** When each worker's last planned task ends, on the run's clock. */
1188
1289
  readonly free: ReadonlyMap<string, number>
1290
+ /** The tasks planned on each worker. */
1291
+ readonly given: ReadonlyMap<string, readonly string[]>
1189
1292
  /** When each worker still starting joins, on the run's clock. */
1190
1293
  readonly join: ReadonlyMap<string, number>
1191
1294
  /** Those of each worker started and not seen yet, in the order started. */
1192
- readonly fresh: readonly { readonly free: number; readonly join: number }[]
1295
+ readonly fresh: readonly {
1296
+ readonly free: number
1297
+ readonly join: number
1298
+ readonly given: readonly string[]
1299
+ }[]
1193
1300
  /** Starts the plan wants later, each when due, on the run's clock. */
1194
1301
  readonly later: readonly {
1195
1302
  readonly size: Size
1196
1303
  readonly at: number
1197
1304
  readonly free: number
1198
1305
  readonly join: number
1306
+ readonly given: readonly string[]
1199
1307
  }[]
1200
1308
  /** Work it saw that has since been seen running: work that left otherwise moves the plan. */
1201
1309
  readonly seen: Set<string>
@@ -1280,12 +1388,18 @@ function forecastOf(
1280
1388
  join: new Map(
1281
1389
  input.workers.flatMap((w) => (w.starting === true ? [[w.id, now + w.at] as const] : [])),
1282
1390
  ),
1283
- fresh: ahead.started.map((a) => ({ free: now + a.freeInMs, join: now + a.joinInMs })),
1391
+ given: ahead.given,
1392
+ fresh: ahead.started.map((a) => ({
1393
+ free: now + a.freeInMs,
1394
+ join: now + a.joinInMs,
1395
+ given: a.given,
1396
+ })),
1284
1397
  later: ahead.later.map((l) => ({
1285
1398
  size: l.size,
1286
1399
  at: now + l.inMs,
1287
1400
  free: now + l.freeInMs,
1288
1401
  join: now + l.inMs + l.joinInMs,
1402
+ given: l.given,
1289
1403
  })),
1290
1404
  seen: new Set(),
1291
1405
  ends: new Map(
@@ -1429,6 +1543,7 @@ function follow(
1429
1543
  }
1430
1544
  const workers = new Set(f.workers)
1431
1545
  const free = new Map(f.free)
1546
+ const given = new Map(f.given)
1432
1547
  const join = new Map(f.join)
1433
1548
  const fresh = [...f.fresh]
1434
1549
  const release: string[] = []
@@ -1439,6 +1554,7 @@ function follow(
1439
1554
  if (next === undefined) return undefined
1440
1555
  workers.add(w.id)
1441
1556
  free.set(w.id, next.free)
1557
+ given.set(w.id, next.given)
1442
1558
  join.set(w.id, next.join)
1443
1559
  }
1444
1560
  // Joined sooner or later than played, past the margin.
@@ -1459,6 +1575,11 @@ function follow(
1459
1575
  // and one let go still counts against its size's max. A Mac let go
1460
1576
  // so left a Mac task no worker, and the run never ended (#4989).
1461
1577
  if (f.until < Infinity) return undefined
1578
+ // A task planned on it not started: its last one ended and the next
1579
+ // not handed to it yet. Let go, that one had no worker and the run
1580
+ // never ended (a two-task chain on a max-1 size, its rest within the
1581
+ // slack). Played again, the plan sees what waits for it.
1582
+ if (given.get(w.id)?.some((id) => !f.seen.has(id)) === true) return undefined
1462
1583
  release.push(w.id)
1463
1584
  workers.delete(w.id)
1464
1585
  }
@@ -1466,7 +1587,7 @@ function follow(
1466
1587
  const start: Size[] = []
1467
1588
  const later = [...f.later]
1468
1589
  while (later.length > 0 && now >= later[0]!.at) {
1469
- const { size, free: ends, join: joins } = later.shift()!
1590
+ const { size, free: ends, join: joins, given: runs } = later.shift()!
1470
1591
  // Its size taken up since: the plan no longer fits the run. Read
1471
1592
  // from what the run started, not from every task: each start read
1472
1593
  // them all (2026-11-05).
@@ -1481,13 +1602,14 @@ function follow(
1481
1602
  return undefined
1482
1603
  } else if (!sizes.some((s) => s.name === size.name)) return undefined
1483
1604
  start.push(size)
1484
- fresh.push({ free: ends, join: joins })
1605
+ fresh.push({ free: ends, join: joins, given: runs })
1485
1606
  }
1486
1607
  const next: Forecast = {
1487
1608
  ...f,
1488
1609
  workers,
1489
1610
  count: f.count + start.length - release.length,
1490
1611
  free,
1612
+ given,
1491
1613
  join,
1492
1614
  fresh,
1493
1615
  later,
@@ -2052,8 +2174,14 @@ export function advise(
2052
2174
  if (simulate(tasks, all.slice(0, mid), how) <= bound) hi = mid
2053
2175
  else lo = mid + 1
2054
2176
  }
2055
- const kept = all.slice(0, lo)
2056
- return { fleets: { fleet: fleetOf(kept, of), endMs: simulate(tasks, kept, how) }, kept }
2177
+ // Then each the run ends as soon without, as the live plan trims.
2178
+ const { kept, before } = dropSpare(
2179
+ all.slice(0, lo),
2180
+ all.slice(0, lo),
2181
+ play(tasks, all.slice(0, lo), how),
2182
+ (ws) => play(tasks, ws, how),
2183
+ )
2184
+ return { fleets: { fleet: fleetOf(kept, of), endMs: before.end }, kept }
2057
2185
  }
2058
2186
  const declared = sizes.length === 0 ? undefined : halved(sizes)?.fleets
2059
2187
  const lifted = sizes.map((s) => ({ ...s, max: Math.max(s.max, capOf(s.cpu, s.platform)) }))