@usine/orchestrator 0.2.1 → 0.4.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.
Files changed (222) hide show
  1. package/LICENSE +661 -21
  2. package/README.md +174 -52
  3. package/dist/board/items.d.ts +22 -3
  4. package/dist/board/items.d.ts.map +1 -1
  5. package/dist/board/items.js +44 -4
  6. package/dist/board/items.js.map +1 -1
  7. package/dist/board/projection.d.ts +8 -1
  8. package/dist/board/projection.d.ts.map +1 -1
  9. package/dist/board/projection.js +63 -14
  10. package/dist/board/projection.js.map +1 -1
  11. package/dist/board/tasks.d.ts +15 -3
  12. package/dist/board/tasks.d.ts.map +1 -1
  13. package/dist/board/tasks.js +28 -9
  14. package/dist/board/tasks.js.map +1 -1
  15. package/dist/cloudflare.d.ts +32 -0
  16. package/dist/cloudflare.d.ts.map +1 -0
  17. package/dist/cloudflare.js +68 -0
  18. package/dist/cloudflare.js.map +1 -0
  19. package/dist/credentials.d.ts +5 -6
  20. package/dist/credentials.d.ts.map +1 -1
  21. package/dist/credentials.js +22 -15
  22. package/dist/credentials.js.map +1 -1
  23. package/dist/current-instance/changes.d.ts +87 -0
  24. package/dist/current-instance/changes.d.ts.map +1 -0
  25. package/dist/current-instance/changes.js +141 -0
  26. package/dist/current-instance/changes.js.map +1 -0
  27. package/dist/current-instance/current-instance.d.ts +89 -0
  28. package/dist/current-instance/current-instance.d.ts.map +1 -0
  29. package/dist/current-instance/current-instance.js +163 -0
  30. package/dist/current-instance/current-instance.js.map +1 -0
  31. package/dist/exe-dev.d.ts +6 -2
  32. package/dist/exe-dev.d.ts.map +1 -1
  33. package/dist/exe-dev.js +10 -9
  34. package/dist/exe-dev.js.map +1 -1
  35. package/dist/http-client.d.ts +8 -0
  36. package/dist/http-client.d.ts.map +1 -0
  37. package/dist/http-client.js +8 -0
  38. package/dist/http-client.js.map +1 -0
  39. package/dist/idle-triggers.d.ts +1 -1
  40. package/dist/idle-triggers.d.ts.map +1 -1
  41. package/dist/idle-triggers.js +1 -1
  42. package/dist/idle-triggers.js.map +1 -1
  43. package/dist/label-reconciler.d.ts +5 -2
  44. package/dist/label-reconciler.d.ts.map +1 -1
  45. package/dist/label-reconciler.js.map +1 -1
  46. package/dist/lease-order.d.ts +60 -0
  47. package/dist/lease-order.d.ts.map +1 -0
  48. package/dist/lease-order.js +76 -0
  49. package/dist/lease-order.js.map +1 -0
  50. package/dist/local.d.ts +4 -18
  51. package/dist/local.d.ts.map +1 -1
  52. package/dist/local.js +9 -45
  53. package/dist/local.js.map +1 -1
  54. package/dist/loop-matches.d.ts +28 -0
  55. package/dist/loop-matches.d.ts.map +1 -0
  56. package/dist/loop-matches.js +74 -0
  57. package/dist/loop-matches.js.map +1 -0
  58. package/dist/operator/attempt.d.ts +76 -10
  59. package/dist/operator/attempt.d.ts.map +1 -1
  60. package/dist/operator/attempt.js +69 -11
  61. package/dist/operator/attempt.js.map +1 -1
  62. package/dist/operator/board.d.ts +48 -16
  63. package/dist/operator/board.d.ts.map +1 -1
  64. package/dist/operator/board.js +48 -17
  65. package/dist/operator/board.js.map +1 -1
  66. package/dist/operator/configuration.d.ts +314 -0
  67. package/dist/operator/configuration.d.ts.map +1 -0
  68. package/dist/operator/configuration.js +490 -0
  69. package/dist/operator/configuration.js.map +1 -0
  70. package/dist/operator/desk.d.ts +206 -0
  71. package/dist/operator/desk.d.ts.map +1 -0
  72. package/dist/operator/desk.js +525 -0
  73. package/dist/operator/desk.js.map +1 -0
  74. package/dist/operator/github-changes.d.ts +88 -0
  75. package/dist/operator/github-changes.d.ts.map +1 -0
  76. package/dist/operator/github-changes.js +266 -0
  77. package/dist/operator/github-changes.js.map +1 -0
  78. package/dist/operator/http.d.ts +135 -105
  79. package/dist/operator/http.d.ts.map +1 -1
  80. package/dist/operator/http.js +155 -30
  81. package/dist/operator/http.js.map +1 -1
  82. package/dist/operator/machines.d.ts +71 -0
  83. package/dist/operator/machines.d.ts.map +1 -0
  84. package/dist/operator/machines.js +103 -0
  85. package/dist/operator/machines.js.map +1 -0
  86. package/dist/operator/need.d.ts +58 -11
  87. package/dist/operator/need.d.ts.map +1 -1
  88. package/dist/operator/need.js +61 -9
  89. package/dist/operator/need.js.map +1 -1
  90. package/dist/operator/operator.d.ts +135 -29
  91. package/dist/operator/operator.d.ts.map +1 -1
  92. package/dist/operator/operator.js +370 -63
  93. package/dist/operator/operator.js.map +1 -1
  94. package/dist/operator/overview.d.ts +7 -7
  95. package/dist/operator/overview.d.ts.map +1 -1
  96. package/dist/operator/overview.js +5 -5
  97. package/dist/operator/overview.js.map +1 -1
  98. package/dist/operator/projection.d.ts +56 -5
  99. package/dist/operator/projection.d.ts.map +1 -1
  100. package/dist/operator/projection.js +108 -9
  101. package/dist/operator/projection.js.map +1 -1
  102. package/dist/operator/run.d.ts +101 -6
  103. package/dist/operator/run.d.ts.map +1 -1
  104. package/dist/operator/run.js +100 -7
  105. package/dist/operator/run.js.map +1 -1
  106. package/dist/operator/statistics/cache.d.ts +7 -0
  107. package/dist/operator/statistics/cache.d.ts.map +1 -0
  108. package/dist/operator/statistics/cache.js +40 -0
  109. package/dist/operator/statistics/cache.js.map +1 -0
  110. package/dist/operator/statistics/runs.d.ts +36 -0
  111. package/dist/operator/statistics/runs.d.ts.map +1 -0
  112. package/dist/operator/statistics/runs.js +284 -0
  113. package/dist/operator/statistics/runs.js.map +1 -0
  114. package/dist/operator/statistics/statistics.d.ts +20 -0
  115. package/dist/operator/statistics/statistics.d.ts.map +1 -0
  116. package/dist/operator/statistics/statistics.js +138 -0
  117. package/dist/operator/statistics/statistics.js.map +1 -0
  118. package/dist/operator/statistics/tasks.d.ts +30 -0
  119. package/dist/operator/statistics/tasks.d.ts.map +1 -0
  120. package/dist/operator/statistics/tasks.js +244 -0
  121. package/dist/operator/statistics/tasks.js.map +1 -0
  122. package/dist/operator/statistics/view.d.ts +302 -0
  123. package/dist/operator/statistics/view.d.ts.map +1 -0
  124. package/dist/operator/statistics/view.js +243 -0
  125. package/dist/operator/statistics/view.js.map +1 -0
  126. package/dist/operator/statistics/window.d.ts +24 -0
  127. package/dist/operator/statistics/window.d.ts.map +1 -0
  128. package/dist/operator/statistics/window.js +47 -0
  129. package/dist/operator/statistics/window.js.map +1 -0
  130. package/dist/operator/task.d.ts +94 -0
  131. package/dist/operator/task.d.ts.map +1 -0
  132. package/dist/operator/task.js +204 -0
  133. package/dist/operator/task.js.map +1 -0
  134. package/dist/operator/transcript.d.ts +12 -12
  135. package/dist/operator/transcript.d.ts.map +1 -1
  136. package/dist/operator/transcript.js +11 -9
  137. package/dist/operator/transcript.js.map +1 -1
  138. package/dist/operator/usage.d.ts +103 -0
  139. package/dist/operator/usage.d.ts.map +1 -0
  140. package/dist/operator/usage.js +234 -0
  141. package/dist/operator/usage.js.map +1 -0
  142. package/dist/orchestrator.d.ts +8 -5
  143. package/dist/orchestrator.d.ts.map +1 -1
  144. package/dist/orchestrator.js +4 -2
  145. package/dist/orchestrator.js.map +1 -1
  146. package/dist/polling/closed-item-asks.d.ts +8 -0
  147. package/dist/polling/closed-item-asks.d.ts.map +1 -0
  148. package/dist/polling/closed-item-asks.js +28 -0
  149. package/dist/polling/closed-item-asks.js.map +1 -0
  150. package/dist/polling/cycle.d.ts +28 -10
  151. package/dist/polling/cycle.d.ts.map +1 -1
  152. package/dist/polling/cycle.js +20 -6
  153. package/dist/polling/cycle.js.map +1 -1
  154. package/dist/polling/evaluation-failures.d.ts +42 -0
  155. package/dist/polling/evaluation-failures.d.ts.map +1 -0
  156. package/dist/polling/evaluation-failures.js +103 -0
  157. package/dist/polling/evaluation-failures.js.map +1 -0
  158. package/dist/polling/hints.d.ts +2 -2
  159. package/dist/polling/hints.d.ts.map +1 -1
  160. package/dist/polling/hints.js +2 -2
  161. package/dist/polling/hints.js.map +1 -1
  162. package/dist/polling/loop-evaluation.d.ts +21 -11
  163. package/dist/polling/loop-evaluation.d.ts.map +1 -1
  164. package/dist/polling/loop-evaluation.js +70 -27
  165. package/dist/polling/loop-evaluation.js.map +1 -1
  166. package/dist/polling/open-issues.d.ts +23 -0
  167. package/dist/polling/open-issues.d.ts.map +1 -0
  168. package/dist/polling/open-issues.js +77 -0
  169. package/dist/polling/open-issues.js.map +1 -0
  170. package/dist/polling/open-pull-requests.d.ts +28 -0
  171. package/dist/polling/open-pull-requests.d.ts.map +1 -0
  172. package/dist/polling/open-pull-requests.js +131 -0
  173. package/dist/polling/open-pull-requests.js.map +1 -0
  174. package/dist/polling/polling.d.ts +20 -11
  175. package/dist/polling/polling.d.ts.map +1 -1
  176. package/dist/polling/polling.js +215 -52
  177. package/dist/polling/polling.js.map +1 -1
  178. package/dist/polling/repository-sync.d.ts +21 -14
  179. package/dist/polling/repository-sync.d.ts.map +1 -1
  180. package/dist/polling/repository-sync.js +35 -28
  181. package/dist/polling/repository-sync.js.map +1 -1
  182. package/dist/polling-triggers.d.ts +1 -1
  183. package/dist/polling-triggers.d.ts.map +1 -1
  184. package/dist/polling-triggers.js +1 -1
  185. package/dist/polling-triggers.js.map +1 -1
  186. package/dist/reloads.d.ts +14 -0
  187. package/dist/reloads.d.ts.map +1 -0
  188. package/dist/reloads.js +7 -0
  189. package/dist/reloads.js.map +1 -0
  190. package/dist/runtime.d.ts +24 -0
  191. package/dist/runtime.d.ts.map +1 -0
  192. package/dist/runtime.js +58 -0
  193. package/dist/runtime.js.map +1 -0
  194. package/dist/schedule-triggers.d.ts +14 -10
  195. package/dist/schedule-triggers.d.ts.map +1 -1
  196. package/dist/schedule-triggers.js +36 -31
  197. package/dist/schedule-triggers.js.map +1 -1
  198. package/dist/scheduler.d.ts +45 -5
  199. package/dist/scheduler.d.ts.map +1 -1
  200. package/dist/scheduler.js +587 -44
  201. package/dist/scheduler.js.map +1 -1
  202. package/dist/ssh.d.ts +103 -0
  203. package/dist/ssh.d.ts.map +1 -0
  204. package/dist/ssh.js +144 -0
  205. package/dist/ssh.js.map +1 -0
  206. package/dist/trigger-condition.d.ts +12 -0
  207. package/dist/trigger-condition.d.ts.map +1 -0
  208. package/dist/trigger-condition.js +88 -0
  209. package/dist/trigger-condition.js.map +1 -0
  210. package/dist/trigger-submission.d.ts +16 -13
  211. package/dist/trigger-submission.d.ts.map +1 -1
  212. package/dist/trigger-submission.js +19 -14
  213. package/dist/trigger-submission.js.map +1 -1
  214. package/dist/worker-engine.d.ts +13 -0
  215. package/dist/worker-engine.d.ts.map +1 -0
  216. package/dist/worker-engine.js +56 -0
  217. package/dist/worker-engine.js.map +1 -0
  218. package/dist/worker-status.d.ts +140 -0
  219. package/dist/worker-status.d.ts.map +1 -0
  220. package/dist/worker-status.js +76 -0
  221. package/dist/worker-status.js.map +1 -0
  222. package/package.json +47 -22
package/dist/scheduler.js CHANGED
@@ -1,108 +1,651 @@
1
1
  import { resolveParentEffort } from "@usine/core/agent";
2
2
  import { AgentFailure, InfraFailure } from "@usine/core/attempt/failure";
3
- import { WorkerName } from "@usine/core/worker";
4
- import { CheckoutRequest, Executor, ExecutorError, } from "@usine/executor";
3
+ import { salvageRefFor } from "@usine/core/attempt/salvage";
4
+ import { canonicalRepositoryName } from "@usine/core/repository";
5
+ import { BotLogin, WorkerName } from "@usine/core/worker";
6
+ import { CheckoutRequest, Executor, ExecutorError, ExecutorOperationError, fullPreflight, } from "@usine/executor";
5
7
  import { StepExecutor } from "@usine/engine/step-executor";
6
- import { DateTime, Deferred, Effect, Layer, Option, Order, Queue, Ref, Schema, TxPriorityQueue, } from "effect";
8
+ import { GitHubApp, isRefusal } from "@usine/github/github-app";
9
+ import { Array as EffectArray, Context, DateTime, Deferred, Duration, Effect, Exit, FiberMap, Layer, Match, Option, Ref, Result, Schema, Semaphore, TxPriorityQueue, TxRef, } from "effect";
10
+ import { agentsWithNewBrains, workersChange, } from "./current-instance/changes.js";
11
+ import { CurrentInstance, reloadPreflightFailed, restartRequired, } from "./current-instance/current-instance.js";
12
+ import { payloadLabels } from "./board/items.js";
13
+ import { aheadCount, leaseOrder, passedCount, pipelinePosition, } from "./lease-order.js";
14
+ import { recordPreflight, unchecked, WorkerStatus, } from "./worker-status.js";
7
15
  export class UnsupportedWorkerError extends Schema.TaggedError("@usine/orchestrator/UnsupportedWorkerError")("UnsupportedWorkerError", {
8
16
  worker: WorkerName,
9
17
  kind: Schema.String,
10
18
  }) {
11
19
  }
12
- const reservationOrder = Order.combine(Order.mapInput(Order.Number, (reservation) => reservation.eventTime), Order.mapInput(Order.Number, (reservation) => reservation.order));
13
- const asAttemptFailure = (error) => error._tag === "WorkerCapabilityError"
14
- ? new AgentFailure({ message: error.message })
15
- : new InfraFailure({
16
- message: error._tag === "ExecutorOperationError"
17
- ? `Executor ${error.operation} failed: ${error.message}`
18
- : error.message,
19
- cause: error,
20
- });
21
- const makeLease = Effect.fn("WorkerScheduler.makeLease")(function* (worker, available) {
22
- const released = yield* Ref.make(false);
23
- return {
24
- worker,
25
- release: Ref.modify(released, (done) => [!done, true]).pipe(Effect.flatMap((first) => (first ? Queue.offer(available, worker) : Effect.void)), Effect.asVoid),
26
- };
20
+ const positionFields = (position) => Match.valueTags(position, {
21
+ Priority: ({ priority }) => ({ positionSource: "priority", position: priority }),
22
+ Lane: ({ lane, number }) => ({ positionSource: "lane", position: number, lane }),
23
+ Unpositioned: () => ({ positionSource: "none" }),
24
+ });
25
+ const slotPoolOf = (workers, previous) => {
26
+ const declared = new Set(workers.map(({ name }) => name));
27
+ return new Map([
28
+ ...workers.map(({ name, maxConcurrentAttempts }) => [
29
+ name,
30
+ { capacity: maxConcurrentAttempts, leased: previous.get(name)?.leased ?? 0 },
31
+ ]),
32
+ ...[...previous]
33
+ .filter(([name, { leased }]) => !declared.has(name) && leased > 0)
34
+ .map(([name, { leased }]) => [name, { capacity: 0, leased }]),
35
+ ]);
36
+ };
37
+ /** Returns the Workers that take new Attempts: those `usine.yaml` declares with slots. */
38
+ const openWorkers = (pool) => [...pool].filter(([, { capacity }]) => capacity > 0).map(([worker]) => worker);
39
+ /** Returns the Runs of `queued`, which is in lease order, with the Runs ahead of each. */
40
+ const queuedRuns = (queued, pool) => {
41
+ const open = openWorkers(pool);
42
+ return queued.map((reservation, index) => ({
43
+ runId: reservation.runId,
44
+ loop: reservation.loop,
45
+ itemKey: reservation.itemKey,
46
+ position: reservation.position,
47
+ ahead: aheadCount(queued.slice(0, index), reservation, open),
48
+ }));
49
+ };
50
+ const isFree = (pool, worker) => {
51
+ const slots = pool.get(worker);
52
+ return slots !== undefined && slots.leased < slots.capacity;
53
+ };
54
+ /**
55
+ * Returns the Worker with a free slot for `reservation`: its specified Worker, or else the first
56
+ * free one that isn't `paused` from creating machines.
57
+ */
58
+ const workerFor = (pool, paused, reservation) => Option.match(reservation.worker, {
59
+ onNone: () => Option.fromNullishOr([...pool.keys()].find((worker) => isFree(pool, worker) && paused.get(worker)?.blocked !== true)),
60
+ onSome: (worker) => (isFree(pool, worker) ? Option.some(worker) : Option.none()),
27
61
  });
62
+ /** The first pause after a machine creation fails on a Worker. It doubles up to `MAX_PAUSE`. */
63
+ const FIRST_PAUSE = Duration.seconds(30);
64
+ const MAX_PAUSE = Duration.minutes(10);
65
+ /** Returns the pause after the `failures`th consecutive failed machine creation. */
66
+ const pauseAfter = (failures) => Duration.min(Duration.times(FIRST_PAUSE, 2 ** Math.min(failures - 1, 10)), MAX_PAUSE);
67
+ const withLeased = (pool, worker, change) => {
68
+ const slots = pool.get(worker);
69
+ if (slots === undefined)
70
+ return pool;
71
+ return new Map(pool).set(worker, { ...slots, leased: slots.leased + change });
72
+ };
73
+ const releasedFrom = (pool, worker) => {
74
+ const slots = pool.get(worker);
75
+ if (slots === undefined)
76
+ return [{ _tag: "Dropped" }, pool];
77
+ if (slots.capacity === 0 && slots.leased <= 1) {
78
+ const next = new Map(pool);
79
+ next.delete(worker);
80
+ return [{ _tag: "Dropped" }, next];
81
+ }
82
+ const next = withLeased(pool, worker, -1);
83
+ return slots.leased > slots.capacity
84
+ ? [{ _tag: "Retired", running: slots.leased - 1, slots: slots.capacity }, next]
85
+ : [{ _tag: "Freed" }, next];
86
+ };
87
+ // Only errors that a retry can avoid become the `InfraFailure` that the engine retries.
88
+ const asAttemptFailure = (error) => Match.valueTags(error, {
89
+ WorkerCapabilityError: ({ message }) => new AgentFailure({ message }),
90
+ CredentialUnavailableError: ({ reason, message }) => reason === "refused"
91
+ ? new AgentFailure({ message })
92
+ : new InfraFailure({ message, cause: error }),
93
+ ExecutorOperationError: ({ operation, message }) => new InfraFailure({ message: `Executor ${operation} failed: ${message}`, cause: error }),
94
+ });
95
+ // The operation error has no Worker field, and reports must include the Worker.
96
+ const namingWorker = (worker) => (error) => error._tag === "ExecutorOperationError"
97
+ ? new ExecutorOperationError({
98
+ operation: error.operation,
99
+ message: `worker "${worker}": ${error.message}`,
100
+ cause: error.cause,
101
+ })
102
+ : error;
28
103
  const validateWorkers = (kind, workers) => {
29
104
  const unsupported = workers.find((worker) => worker.kind !== kind);
30
105
  return unsupported === undefined
31
106
  ? Effect.succeed(workers)
32
107
  : new UnsupportedWorkerError({ worker: unsupported.name, kind: unsupported.kind });
33
108
  };
34
- /** Schedules Attempts FIFO over the configured Workers and adapts the Executor to the engine's StepExecutor port. */
35
- export const stepExecutorLayer = (config, brains) => Layer.effect(StepExecutor, Effect.gen(function* () {
109
+ const brainLabel = ({ harness, model, effort }) => `${harness}/${model}/${effort}`;
110
+ /** The login that exe.dev Workers write to GitHub as, through exe.dev's integration (ADR 0011). */
111
+ const EXE_DEV_BOT_LOGIN = BotLogin.make("exe-dev-github-integration[bot]");
112
+ /** Returns the Attempt's failure for an unreadable bot account. A refusal isn't retried. */
113
+ const botAccountFailure = (error) => {
114
+ const message = `Can't read the GitHub App's bot account, which authors the attempt's commits: ${error.message}`;
115
+ return isRefusal(error)
116
+ ? new AgentFailure({ message })
117
+ : new InfraFailure({ message, cause: error });
118
+ };
119
+ /** The Worker set of an executor that runs any Worker of its kind by name. */
120
+ const anyWorker = { add: () => Effect.void, remove: () => Effect.void };
121
+ /**
122
+ * Schedules Attempts over the Workers in `leaseOrder` and adapts the Executor to the engine's
123
+ * `StepExecutor` port. A removed Worker stays until its last Attempt ends.
124
+ */
125
+ export const stepExecutorLayer = (config, brains, options = {}) => Layer.effectContext(Effect.gen(function* () {
36
126
  const executor = yield* Executor;
127
+ const instances = yield* CurrentInstance;
128
+ const app = yield* GitHubApp;
37
129
  const workers = yield* validateWorkers(executor.kind, config.workers);
38
- yield* Effect.forEach(workers, ({ name }) => executor.preflight(name, brains), {
130
+ const pool = yield* TxRef.make(slotPoolOf(workers, new Map()));
131
+ const pending = yield* TxPriorityQueue.empty(leaseOrder);
132
+ const nextOrder = yield* Ref.make(0);
133
+ const preflights = yield* Ref.make(new Map());
134
+ const executorWorkers = options.workers ?? anyWorker;
135
+ // Every change to the executor's Workers takes this permit, so a removal never undoes an
136
+ // addition.
137
+ const membership = yield* Semaphore.make(1);
138
+ const adding = yield* Ref.make(new Set());
139
+ const machines = options.machines;
140
+ const paused = yield* TxRef.make(new Map());
141
+ // The machine of each Run's lease, which `execute` gives to the Run's Attempt.
142
+ const runMachines = yield* Ref.make(new Map());
143
+ // The machine creation of each reservation, by its order.
144
+ const creations = yield* FiberMap.make();
145
+ const provisions = (worker) => machines === undefined ? Effect.succeed(false) : machines.provisions(worker);
146
+ const recordPreflightExit = Effect.fn("WorkerScheduler.recordPreflight")(function* (worker, roster, exit) {
147
+ const outcome = Exit.isSuccess(exit)
148
+ ? Option.some(Result.succeed(exit.value))
149
+ : Option.map(Exit.findErrorOption(exit), (error) => Result.fail(error));
150
+ // An interrupted or defective preflight gives no information about the Worker.
151
+ if (Option.isNone(outcome))
152
+ return;
153
+ const checkedAt = yield* DateTime.now;
154
+ yield* Ref.update(preflights, (current) => new Map(current).set(worker, recordPreflight(current.get(worker) ?? unchecked(worker), roster, checkedAt, outcome.value)));
155
+ });
156
+ /**
157
+ * Runs `before`, then the Worker's preflight, and records both outcomes as the Worker's
158
+ * preflight.
159
+ */
160
+ const preflightWorker = (worker, roster, before = Effect.void) => before.pipe(Effect.andThen(executor.preflight(worker.name, roster, worker.maxConcurrentAttempts)), Effect.mapError(namingWorker(worker.name)), Effect.onExit((exit) => recordPreflightExit(worker.name, roster, exit)));
161
+ /**
162
+ * Removes `worker` from the executor unless the scheduler has it or a reload is adding it.
163
+ * Callers must hold `membership`.
164
+ */
165
+ const removeUnused = Effect.fn("WorkerScheduler.removeFromExecutor")(function* (worker) {
166
+ yield* Effect.annotateCurrentSpan({ worker });
167
+ const scheduled = (yield* TxRef.get(pool)).has(worker);
168
+ const added = (yield* Ref.get(adding)).has(worker);
169
+ if (scheduled || added) {
170
+ yield* Effect.logDebug("The Worker stays with the executor, because the scheduler has it or a reload is adding it", { worker, scheduled, added });
171
+ return;
172
+ }
173
+ yield* executorWorkers.remove(worker);
174
+ });
175
+ /**
176
+ * Ends a reload's addition of `added` and removes each Worker that the scheduler doesn't
177
+ * have.
178
+ */
179
+ const settleAdded = (added, removed) => membership.withPermit(Ref.update(adding, (current) => new Set([...current].filter((name) => !added.has(name)))).pipe(Effect.andThen(Effect.forEach(removed, removeUnused, { discard: true }))));
180
+ const unservedChange = (worker) => worker.kind === executor.kind
181
+ ? Option.none()
182
+ : Option.some({
183
+ block: "workers",
184
+ field: `workers.${worker.name}.kind`,
185
+ reason: `worker "${worker.name}" has kind ${worker.kind}, and the running workers have kind ${executor.kind}`,
186
+ });
187
+ const applyWorkers = Effect.fn("WorkerScheduler.applyWorkers")(function* (change, declared) {
188
+ const before = yield* TxRef.modify(pool, (current) => [
189
+ current,
190
+ slotPoolOf(declared, current),
191
+ ]);
192
+ const running = (worker) => before.get(worker)?.leased ?? 0;
193
+ yield* Effect.forEach(change.resized, ({ worker, from }) => Effect.logInfo("Worker slots resized by a reload; running Attempts continue", {
194
+ worker: worker.name,
195
+ slotsBefore: from,
196
+ slotsAfter: worker.maxConcurrentAttempts,
197
+ running: running(worker.name),
198
+ }));
199
+ yield* Effect.forEach(change.added, (worker) => Effect.logInfo("Worker added by a reload; it takes Attempts now", {
200
+ worker: worker.name,
201
+ slots: worker.maxConcurrentAttempts,
202
+ }));
203
+ yield* Effect.forEach(change.removed, ({ name }) => running(name) === 0
204
+ ? Effect.logInfo("Worker removed by a reload; it runs no Attempt, so it leaves the scheduler now", { worker: name })
205
+ : Effect.logInfo("Worker removed by a reload; it takes no new Attempt, and its running Attempts finish", { worker: name, running: running(name) }));
206
+ yield* settleAdded(new Set(change.added.map(({ name }) => name)), change.removed.map(({ name }) => name));
207
+ });
208
+ const prepareReload = Effect.fn("WorkerScheduler.prepareReload")(function* (running, loaded) {
209
+ const change = workersChange(running.config.workers, loaded.config.workers);
210
+ const restart = [
211
+ ...change.restart,
212
+ ...EffectArray.getSomes(change.added.map(unservedChange)),
213
+ ];
214
+ if (EffectArray.isReadonlyArrayNonEmpty(restart)) {
215
+ return yield* restartRequired(restart);
216
+ }
217
+ const agents = agentsWithNewBrains(running, loaded);
218
+ const agentNames = agents.map(({ name }) => name);
219
+ const newBrains = EffectArray.dedupe(agents.map(({ agent }) => agent.brain));
220
+ const roster = EffectArray.dedupe(loaded.agents.map(({ agent }) => agent.brain));
221
+ const wholeRoster = new Set([
222
+ ...change.added.map(({ name }) => name),
223
+ ...change.resized
224
+ .filter(({ worker, from }) => worker.maxConcurrentAttempts > from)
225
+ .map(({ worker }) => worker.name),
226
+ ]);
227
+ const checks = loaded.config.workers.flatMap((worker) => wholeRoster.has(worker.name)
228
+ ? [{ worker, brains: roster }]
229
+ : newBrains.length > 0
230
+ ? [{ worker, brains: newBrains }]
231
+ : []);
232
+ yield* Effect.annotateCurrentSpan({ agents: agentNames, workers: [...wholeRoster] });
233
+ if (checks.length === 0) {
234
+ yield* Effect.logDebug("The reload adds no Worker, slot, or brain, so it runs no Worker preflight");
235
+ return applyWorkers(change, loaded.config.workers);
236
+ }
237
+ const added = new Set(change.added.map(({ name }) => name));
238
+ yield* membership.withPermit(Ref.update(adding, (current) => new Set([...current, ...added])));
239
+ yield* Effect.logInfo("Preflighting the Workers, slots, and brains the reload adds", {
240
+ agents: agentNames,
241
+ brains: newBrains.map(brainLabel),
242
+ workers: [...wholeRoster],
243
+ });
244
+ // Add the Worker to the executor first, so its preflight can connect to its machine.
245
+ yield* Effect.forEach(checks, ({ worker, brains }) => added.has(worker.name)
246
+ ? preflightWorker(worker, brains, executorWorkers.add(worker))
247
+ : preflightWorker(worker, brains), { concurrency: "unbounded", discard: true }).pipe(Effect.mapError((cause) => reloadPreflightFailed(agentNames, [...wholeRoster], cause)), Effect.onExit((exit) => Exit.isFailure(exit) ? settleAdded(added, [...added]) : Effect.void));
248
+ return applyWorkers(change, loaded.config.workers);
249
+ });
250
+ yield* instances.registerWorkers(prepareReload);
251
+ // A reload can change the roster after the caller read `brains` and before the registration.
252
+ const { agents } = yield* instances.get;
253
+ const startupRoster = EffectArray.dedupe([
254
+ ...brains,
255
+ ...agents.map(({ agent }) => agent.brain),
256
+ ]);
257
+ yield* Effect.forEach(workers, (worker) => preflightWorker(worker, startupRoster), {
39
258
  concurrency: "unbounded",
40
259
  discard: true,
260
+ }).pipe(Effect.withSpan("WorkerScheduler.preflight"));
261
+ const releaseSlot = (worker) => TxRef.modify(pool, (current) => releasedFrom(current, worker)).pipe(Effect.flatMap((release) => Match.valueTags(release, {
262
+ Freed: () => Effect.logDebug("Worker slot released", { worker }),
263
+ Retired: ({ running, slots }) => Effect.logInfo("An Attempt ended on a Worker that runs more Attempts than it has slots; its slot retires", { worker, running, slots }),
264
+ Dropped: () => Effect.logInfo("The last Attempt on a removed Worker ended; the Worker leaves the scheduler", { worker }).pipe(Effect.andThen(membership.withPermit(removeUnused(worker)))),
265
+ })));
266
+ /** Deletes the machine that `machine` holds, if any, before the slot frees. */
267
+ const releaseMachine = (worker, machine) => Ref.getAndSet(machine, Option.none()).pipe(Effect.flatMap((hold) => Option.match(hold, {
268
+ onNone: () => Effect.void,
269
+ onSome: (held) => machines === undefined
270
+ ? Effect.void
271
+ : Effect.logDebug("Deleting the machine of the released slot", {
272
+ worker,
273
+ hold: held,
274
+ }).pipe(Effect.andThen(machines.release(held))),
275
+ })));
276
+ const makeLease = Effect.fn("WorkerScheduler.makeLease")(function* (worker, hold = Option.none(), runId = Option.none()) {
277
+ const released = yield* Ref.make(false);
278
+ const machine = yield* Ref.make(hold);
279
+ if (Option.isSome(runId))
280
+ yield* Ref.update(runMachines, (current) => new Map(current).set(runId.value, machine));
281
+ const forget = Option.match(runId, {
282
+ onNone: () => Effect.void,
283
+ onSome: (run) => Ref.update(runMachines, (current) => {
284
+ if (current.get(run) !== machine)
285
+ return current;
286
+ const next = new Map(current);
287
+ next.delete(run);
288
+ return next;
289
+ }),
290
+ });
291
+ return {
292
+ worker,
293
+ release: Ref.modify(released, (done) => [!done, true]).pipe(Effect.flatMap((first) => first
294
+ ? forget.pipe(Effect.andThen(releaseMachine(worker, machine)), Effect.andThen(releaseSlot(worker)))
295
+ : Effect.void)),
296
+ };
297
+ });
298
+ // Retries until a free slot can take a waiting reservation, then takes both. A reservation
299
+ // whose Worker has no free slot doesn't block the ones behind it.
300
+ const nextAssignment = Effect.tx(Effect.gen(function* () {
301
+ const current = yield* TxRef.get(pool);
302
+ const pauses = yield* TxRef.get(paused);
303
+ const waiting = yield* TxPriorityQueue.toArray(pending);
304
+ const next = EffectArray.findFirst(waiting, (reservation) => Option.map(workerFor(current, pauses, reservation), (worker) => ({
305
+ worker,
306
+ reservation,
307
+ })));
308
+ if (Option.isNone(next))
309
+ return yield* Effect.txRetry;
310
+ const { worker, reservation } = next.value;
311
+ yield* TxPriorityQueue.removeIf(pending, (queued) => queued === reservation);
312
+ yield* TxRef.set(pool, withLeased(current, worker, 1));
313
+ return { worker, reservation, passed: passedCount(waiting, reservation) };
314
+ }));
315
+ /** Pauses machine creation on `worker` after `failure`, and resumes it when the pause ends. */
316
+ const pauseWorker = Effect.fn("WorkerScheduler.pauseWorker")(function* (worker, failure) {
317
+ const failedAt = yield* DateTime.now;
318
+ const pause = yield* Effect.tx(TxRef.modify(paused, (current) => {
319
+ const failures = (current.get(worker)?.failures ?? 0) + 1;
320
+ const delay = pauseAfter(failures);
321
+ const next = {
322
+ worker,
323
+ reason: failure._tag === "MachineUnavailableError" ? failure.reason : "check",
324
+ message: failure.message,
325
+ failures,
326
+ failedAt,
327
+ retryAt: DateTime.addDuration(failedAt, delay),
328
+ blocked: true,
329
+ };
330
+ return [next, new Map(current).set(worker, next)];
331
+ }));
332
+ yield* Effect.logWarning("A provisioned Worker could not create a machine; its Runs keep waiting, and it tries again after a pause", {
333
+ worker,
334
+ reason: pause.reason,
335
+ message: pause.message,
336
+ failures: pause.failures,
337
+ retryAt: DateTime.formatIso(pause.retryAt),
338
+ });
339
+ yield* Effect.sleep(DateTime.distance(failedAt, pause.retryAt)).pipe(Effect.andThen(Effect.tx(TxRef.update(paused, (current) => {
340
+ const latest = current.get(worker);
341
+ return latest === undefined || latest.failures !== pause.failures
342
+ ? current
343
+ : new Map(current).set(worker, { ...latest, blocked: false });
344
+ }))), Effect.andThen(Effect.logInfo("A provisioned Worker's pause ended; it creates machines again", {
345
+ worker,
346
+ failures: pause.failures,
347
+ })), Effect.forkScoped);
348
+ });
349
+ /**
350
+ * Creates the machine of `reservation`'s next Attempt on `worker`, whose slot it holds, and
351
+ * then assigns the lease with the machine. On failure, frees the slot, pauses the Worker,
352
+ * and puts the reservation back in the queue, so the Run keeps waiting.
353
+ */
354
+ const provision = Effect.fn("WorkerScheduler.provision")(function* (worker, reservation, passed) {
355
+ const fields = {
356
+ runId: reservation.runId,
357
+ loop: reservation.loop,
358
+ itemKey: reservation.itemKey,
359
+ worker,
360
+ };
361
+ yield* Effect.annotateCurrentSpan(fields);
362
+ if (machines === undefined)
363
+ return yield* Effect.die(new Error("no machines to create"));
364
+ const { agents } = yield* instances.get;
365
+ const roster = EffectArray.dedupe(agents.map(({ agent }) => agent.brain));
366
+ yield* Effect.logInfo("Worker slot taken in lease order; creating the Attempt's machine", {
367
+ ...fields,
368
+ ...positionFields(reservation.position),
369
+ passed,
370
+ });
371
+ const created = yield* machines.create(worker, reservation.runId, roster).pipe(Effect.result, Effect.onInterrupt(() => Effect.logInfo("The Run stopped waiting while its machine was created; the slot is free again", fields).pipe(Effect.andThen(releaseSlot(worker)))));
372
+ yield* Effect.uninterruptible(Effect.gen(function* () {
373
+ if (Result.isFailure(created)) {
374
+ const failure = created.failure;
375
+ yield* releaseSlot(worker);
376
+ if (failure._tag === "WorkerCapabilityError")
377
+ yield* recordPreflightExit(worker, roster, Exit.fail(failure));
378
+ yield* pauseWorker(worker, failure);
379
+ const requeued = yield* Ref.modify(reservation.state, (state) => state._tag === "Creating"
380
+ ? [true, { _tag: "Pending" }]
381
+ : [false, state]);
382
+ if (requeued)
383
+ yield* Effect.tx(TxPriorityQueue.offer(pending, reservation));
384
+ return;
385
+ }
386
+ const grant = created.success;
387
+ yield* Effect.tx(TxRef.update(paused, (current) => {
388
+ if (!current.has(worker))
389
+ return current;
390
+ const next = new Map(current);
391
+ next.delete(worker);
392
+ return next;
393
+ }));
394
+ if (Option.isSome(grant.checked)) {
395
+ const { brains, containerLimits } = grant.checked.value;
396
+ yield* recordPreflightExit(worker, brains, Exit.succeed({ ...fullPreflight, containerLimits }));
397
+ }
398
+ const lease = yield* makeLease(worker, Option.some(grant.hold), Option.some(reservation.runId));
399
+ const assign = yield* Ref.modify(reservation.state, (state) => state._tag === "Creating"
400
+ ? [true, { _tag: "Assigned", lease }]
401
+ : [false, state]);
402
+ if (!assign) {
403
+ yield* Effect.logInfo("The Run stopped waiting while its machine was created; the machine is deleted and the slot is free again", { ...fields, machine: grant.id });
404
+ return yield* lease.release;
405
+ }
406
+ yield* Effect.logInfo("Worker slot leased with a new machine", {
407
+ ...fields,
408
+ machine: grant.id,
409
+ provider: grant.provider,
410
+ });
411
+ yield* Deferred.succeed(reservation.deferred, lease);
412
+ }));
41
413
  });
42
- const available = yield* Queue.make();
43
- yield* Queue.offerAll(available, workers.flatMap(({ maxConcurrentAttempts, name }) => Array.from({ length: maxConcurrentAttempts }, () => name)));
44
- const pending = yield* TxPriorityQueue.empty(reservationOrder);
45
- const nextOrder = yield* Ref.make(0);
46
414
  const dispatch = Effect.forever(Effect.gen(function* () {
47
- const worker = yield* Queue.take(available);
48
- const reservation = yield* TxPriorityQueue.take(pending);
49
- const lease = yield* makeLease(worker, available);
415
+ const { worker, reservation, passed } = yield* nextAssignment;
416
+ // A reattached Attempt keeps its machine, so only a new Attempt gets one.
417
+ if (Option.isNone(reservation.worker) && (yield* provisions(worker))) {
418
+ const creating = yield* Ref.modify(reservation.state, (state) => state._tag === "Pending"
419
+ ? [true, { _tag: "Creating", worker }]
420
+ : [false, state]);
421
+ if (creating)
422
+ yield* FiberMap.run(creations, reservation.order, provision(worker, reservation, passed));
423
+ else
424
+ yield* releaseSlot(worker);
425
+ return;
426
+ }
427
+ const lease = yield* makeLease(worker, Option.none(), Option.some(reservation.runId));
50
428
  const assign = yield* Ref.modify(reservation.state, (state) => state._tag === "Pending"
51
429
  ? [true, { _tag: "Assigned", lease }]
52
430
  : [false, state]);
431
+ const fields = {
432
+ runId: reservation.runId,
433
+ loop: reservation.loop,
434
+ itemKey: reservation.itemKey,
435
+ worker,
436
+ };
53
437
  if (assign) {
54
- yield* Effect.logDebug("Worker slot assigned", { worker });
438
+ yield* Effect.logInfo("Worker slot leased in lease order", {
439
+ ...fields,
440
+ ...positionFields(reservation.position),
441
+ passed,
442
+ });
55
443
  yield* Deferred.succeed(reservation.deferred, lease);
56
444
  }
57
445
  else {
446
+ yield* Effect.logDebug("The Run stopped waiting before its lease; the Worker slot is free again", fields);
58
447
  yield* lease.release;
59
448
  }
60
449
  }));
61
450
  yield* Effect.forkScoped(dispatch);
62
- const reserve = Effect.fn("WorkerScheduler.reserve")(function* (submission) {
451
+ const occupy = Effect.fn("WorkerScheduler.occupy")(function* (worker, attemptId) {
452
+ yield* Effect.annotateCurrentSpan({ worker, attemptId });
453
+ // The Attempt runs on the Worker even without a free slot, so count it now. A slot over
454
+ // capacity retires when an Attempt there ends.
455
+ const slots = yield* TxRef.modify(pool, (current) => [current.get(worker), withLeased(current, worker, 1)]);
456
+ if (slots === undefined) {
457
+ yield* Effect.logWarning("An Attempt already runs on a Worker the scheduler does not have; it occupies no slot, and that Worker takes no new Attempt", { worker });
458
+ return { worker, release: Effect.void };
459
+ }
460
+ const fields = { worker, running: slots.leased + 1, slots: slots.capacity };
461
+ if (slots.leased < slots.capacity)
462
+ yield* Effect.logInfo("An Attempt that already runs on the Worker occupies a slot", fields);
463
+ else
464
+ yield* Effect.logWarning("An Attempt that already runs on the Worker occupies a slot beyond its max_concurrent_attempts; the Worker takes no new Attempt until enough of its Attempts end", fields);
465
+ const hold = machines !== undefined && (yield* provisions(worker))
466
+ ? yield* machines.adopt(worker, attemptId)
467
+ : Option.none();
468
+ return yield* makeLease(worker, hold);
469
+ });
470
+ /**
471
+ * Returns the pipeline position of the Run of `submission`, from the current Loop file and
472
+ * the lanes of the Run's repository.
473
+ */
474
+ const positionOf = Effect.fn("WorkerScheduler.position")(function* (runId, submission) {
475
+ yield* Effect.annotateCurrentSpan({ runId, loop: submission.loop });
476
+ const { config, loops } = yield* instances.get;
477
+ const loop = loops.find(({ name }) => name === submission.loop);
478
+ const priority = Option.flatMap(Option.fromNullishOr(loop), ({ priority }) => priority);
479
+ const lanes = config.repos.find(({ name }) => canonicalRepositoryName(name) === canonicalRepositoryName(submission.repository))?.lanes ?? [];
480
+ const stream = loop?.trigger.on;
481
+ // Only issue and pull request Events record Item labels.
482
+ if (Option.isSome(priority) ||
483
+ lanes.length === 0 ||
484
+ stream === undefined ||
485
+ stream === "schedule") {
486
+ return pipelinePosition(priority, lanes, []);
487
+ }
488
+ const labels = yield* payloadLabels(stream, submission.event.payload).pipe(Effect.catch((error) => Effect.logWarning("The Run's Event has no labels to read; the Run waits for a worker slot without a lane position", {
489
+ runId,
490
+ loop: submission.loop,
491
+ eventId: submission.event.id,
492
+ errorTag: error._tag,
493
+ message: error.message,
494
+ }).pipe(Effect.as([]))));
495
+ return pipelinePosition(priority, lanes, labels);
496
+ });
497
+ const reserve = Effect.fn("WorkerScheduler.reserve")(function* (runId, submission, worker) {
498
+ const run = { runId, loop: submission.loop, itemKey: submission.itemKey };
499
+ yield* Effect.annotateCurrentSpan(run);
500
+ if (Option.isSome(worker))
501
+ yield* Effect.annotateCurrentSpan({ worker: worker.value });
63
502
  const deferred = yield* Deferred.make();
64
503
  const state = yield* Ref.make({ _tag: "Pending" });
65
504
  const reservation = {
505
+ ...run,
66
506
  deferred,
507
+ worker,
508
+ position: yield* positionOf(runId, submission),
67
509
  eventTime: DateTime.toEpochMillis(submission.event.receivedAt),
68
510
  order: yield* Ref.getAndUpdate(nextOrder, (value) => value + 1),
69
511
  state,
70
512
  };
71
- yield* TxPriorityQueue.offer(pending, reservation);
72
- yield* Effect.logInfo("Run waits for a worker slot", {
73
- loop: submission.loop,
74
- itemKey: submission.itemKey,
75
- queued: yield* TxPriorityQueue.size(pending),
76
- });
513
+ const [queue, current] = yield* Effect.tx(TxPriorityQueue.offer(pending, reservation).pipe(Effect.andThen(Effect.all([TxPriorityQueue.toArray(pending), TxRef.get(pool)]))));
514
+ const fields = {
515
+ ...run,
516
+ ...positionFields(reservation.position),
517
+ queued: queue.length,
518
+ ahead: aheadCount(queue.slice(0, queue.indexOf(reservation)), reservation, openWorkers(current)),
519
+ };
520
+ if (Option.isNone(worker))
521
+ yield* Effect.logInfo("Run waits for a worker slot", fields);
522
+ else if ((current.get(worker.value)?.capacity ?? 0) > 0)
523
+ yield* Effect.logInfo("Run waits for a slot on the Worker its reattached Attempt runs on", { ...fields, worker: worker.value });
524
+ else
525
+ yield* Effect.logWarning("Run waits for a slot on the Worker its reattached Attempt runs on, which usine.yaml does not declare; it waits until a reload declares that Worker again or the Run is canceled", { ...fields, worker: worker.value });
77
526
  return yield* Deferred.await(deferred).pipe(Effect.onInterrupt(() => Ref.modify(state, (current) => current._tag === "Assigned"
78
527
  ? [Option.some(current.lease), { _tag: "Canceled" }]
79
- : [Option.none(), { _tag: "Canceled" }]).pipe(Effect.flatMap(Option.match({ onNone: () => Effect.void, onSome: (lease) => lease.release })))));
528
+ : [Option.none(), { _tag: "Canceled" }]).pipe(
529
+ // Stops a machine creation for the Run, which deletes the machine and frees the slot.
530
+ Effect.tap(() => FiberMap.remove(creations, reservation.order)),
531
+ // Remove a canceled reservation from the queue, so one whose Worker never frees a
532
+ // slot doesn't stay there.
533
+ Effect.tap(() => TxPriorityQueue.removeIf(pending, (waiting) => waiting === reservation)), Effect.flatMap(Option.match({ onNone: () => Effect.void, onSome: (lease) => lease.release })))));
534
+ });
535
+ /**
536
+ * Gives a provisioned Worker's machine to the Attempt of `execution`: the machine its Run's
537
+ * lease created, or for a reattachment the machine it ran on before the restart.
538
+ */
539
+ const machineForAttempt = Effect.fn("WorkerScheduler.machineForAttempt")(function* (execution) {
540
+ const worker = execution.attempt.worker;
541
+ if (machines === undefined || !(yield* provisions(worker)))
542
+ return;
543
+ const attemptId = execution.attempt.id;
544
+ const fields = { runId: execution.run.id, attemptId, worker };
545
+ const machine = (yield* Ref.get(runMachines)).get(execution.run.id);
546
+ if (execution.reattach) {
547
+ const adopted = yield* machines.adopt(worker, attemptId);
548
+ if (machine !== undefined && Option.isNone(yield* Ref.get(machine)))
549
+ yield* Ref.set(machine, adopted);
550
+ return yield* Effect.logDebug("The reattached Attempt keeps its machine", {
551
+ ...fields,
552
+ found: Option.isSome(adopted),
553
+ });
554
+ }
555
+ const hold = machine === undefined ? Option.none() : yield* Ref.get(machine);
556
+ if (Option.isNone(hold))
557
+ return yield* new InfraFailure({
558
+ message: `Worker "${worker}" created no machine for the attempt`,
559
+ cause: new Error("no machine in the Run's lease"),
560
+ });
561
+ yield* machines.assign(hold.value, attemptId).pipe(Effect.mapError((error) => new InfraFailure({
562
+ message: `Can't give the machine to the attempt: ${error.message}`,
563
+ cause: error,
564
+ })));
80
565
  });
566
+ // Every Worker kind commits as the App's bot account; only exe.dev writes to GitHub through
567
+ // another bot.
568
+ const writerFor = Effect.fn("WorkerScheduler.writer")(function* (attemptId) {
569
+ yield* Effect.annotateCurrentSpan({ attemptId });
570
+ const commitAuthor = yield* app.botAccount();
571
+ const botLogin = executor.kind === "exe.dev" ? EXE_DEV_BOT_LOGIN : commitAuthor.login;
572
+ return { commitAuthor, botLogin };
573
+ }, (effect, attemptId) => effect.pipe(Effect.tapError((error) => Effect.logWarning(isRefusal(error)
574
+ ? "Could not read the App's bot account, which authors the Attempt's commits; the Attempt fails without an automatic retry"
575
+ : "Could not read the App's bot account, which authors the Attempt's commits; the Attempt fails with an infrastructure failure, which is retried", { attemptId, errorTag: error._tag, message: error.message })), Effect.mapError(botAccountFailure)));
81
576
  const execute = Effect.fn("WorkerScheduler.execute")(function* (execution, io) {
82
577
  yield* Effect.annotateCurrentSpan({
83
578
  attemptId: execution.attempt.id,
84
579
  runId: execution.run.id,
85
580
  worker: execution.attempt.worker,
581
+ loop: execution.run.loop,
582
+ permissions: execution.permissions,
583
+ reattach: execution.reattach,
86
584
  });
585
+ const { settings } = (yield* instances.get).config;
586
+ const { commitAuthor, botLogin } = yield* writerFor(execution.attempt.id);
587
+ yield* machineForAttempt(execution);
87
588
  const prepared = yield* executor
88
589
  .prepare(new CheckoutRequest({
89
590
  attemptId: execution.attempt.id,
90
591
  worker: execution.attempt.worker,
91
592
  repository: execution.repository,
593
+ loop: execution.run.loop,
594
+ permissions: execution.permissions,
92
595
  revision: execution.revision,
93
596
  payload: execution.event.payload,
94
597
  task: execution.task,
95
598
  agent: execution.agent,
96
599
  parentModel: execution.parentModel,
97
- maxChildren: config.settings.maxChildren,
98
- maxAttemptMinutes: config.settings.maxAttemptMinutes,
99
- maxAttemptTokens: config.settings.maxAttemptTokens,
100
- parentEffort: resolveParentEffort(execution.agent, config.settings.parentEffort),
600
+ maxChildren: settings.maxChildren,
601
+ maxAttemptMinutes: settings.maxAttemptMinutes,
602
+ maxAttemptTokens: settings.maxAttemptTokens,
603
+ parentEffort: resolveParentEffort(execution.agent, settings.parentEffort),
604
+ salvageRef: salvageRefFor(execution.run.id, execution.attempt.number),
605
+ resume: execution.resume,
606
+ reattach: execution.reattach,
607
+ botLogin: Option.some(botLogin),
608
+ commitAuthor,
101
609
  }))
102
- .pipe(Effect.mapError(asAttemptFailure));
610
+ .pipe(Effect.tapError((error) => error._tag === "CredentialUnavailableError"
611
+ ? Effect.logWarning(error.reason === "refused"
612
+ ? "The Attempt's repository access was refused; the Attempt fails without an automatic retry"
613
+ : "The Attempt's repository access could not be obtained now; the Attempt fails with an infrastructure failure, which is retried", {
614
+ attemptId: execution.attempt.id,
615
+ worker: execution.attempt.worker,
616
+ repository: error.repository,
617
+ reason: error.reason,
618
+ message: error.message,
619
+ })
620
+ : Effect.void), Effect.mapError(asAttemptFailure));
621
+ // The engine reconciles a checkout kept for a reattachment until preparation acquires it;
622
+ // from here the executor releases it.
623
+ yield* io.checkoutAcquired;
103
624
  return yield* prepared.launch(io);
104
625
  }, Effect.scoped);
105
- const interrupt = (attemptId) => executor.cancel(attemptId).pipe(Effect.mapError(asAttemptFailure));
106
- return StepExecutor.of({ execute, interrupt, reserve, intervene: executor.intervene });
626
+ const interrupt = (attemptId, reason) => executor.cancel(attemptId, reason).pipe(Effect.mapError(asAttemptFailure));
627
+ const status = WorkerStatus.of({
628
+ slots: TxRef.get(pool).pipe(Effect.map((current) => [...current].map(([worker, { capacity, leased }]) => ({ worker, capacity, leased })))),
629
+ queue: Effect.tx(Effect.all([TxPriorityQueue.toArray(pending), TxRef.get(pool)])).pipe(Effect.map(([queued, current]) => queuedRuns(queued, current))),
630
+ preflights: Ref.get(preflights).pipe(Effect.map((current) => [...current.values()])),
631
+ machines: machines === undefined ? Effect.succeed([]) : machines.machines,
632
+ provisioning: TxRef.get(paused).pipe(Effect.map((current) => [...current.values()])),
633
+ });
634
+ const stepExecutor = StepExecutor.of({
635
+ workers: TxRef.get(pool).pipe(Effect.map((current) => [...current.keys()])),
636
+ execute,
637
+ interrupt,
638
+ isLive: executor.isLive,
639
+ outcome: executor.outcome,
640
+ reserve,
641
+ occupy,
642
+ sweep: Effect.andThen(executor.sweep, machines === undefined ? Effect.void : machines.sweep),
643
+ discard: options.discard ?? (() => Effect.void),
644
+ intervene: executor.intervene,
645
+ recover: (attempt, record) => executor
646
+ .recover(attempt, record, app.botAccount().pipe(Effect.mapError(botAccountFailure)))
647
+ .pipe(Effect.catchIf(Schema.is(ExecutorError), (error) => Effect.fail(asAttemptFailure(error)))),
648
+ });
649
+ return Context.make(StepExecutor, stepExecutor).pipe(Context.add(WorkerStatus, status));
107
650
  }));
108
651
  //# sourceMappingURL=scheduler.js.map