@foldrun/core 0.2.0 → 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 (85) hide show
  1. package/README.md +31 -10
  2. package/dist/src/approvals.d.ts +104 -1
  3. package/dist/src/approvals.js +192 -2
  4. package/dist/src/budget.d.ts +32 -0
  5. package/dist/src/budget.js +177 -0
  6. package/dist/src/catalog.js +2 -1
  7. package/dist/src/confine.js +6 -1
  8. package/dist/src/deploy.js +4 -2
  9. package/dist/src/egress.d.ts +19 -2
  10. package/dist/src/egress.js +2 -1
  11. package/dist/src/evals.d.ts +4 -0
  12. package/dist/src/evals.js +20 -11
  13. package/dist/src/flow-lint.d.ts +4 -0
  14. package/dist/src/flow-lint.js +108 -0
  15. package/dist/src/host-env.d.ts +8 -0
  16. package/dist/src/host-env.js +48 -0
  17. package/dist/src/json-schema.d.ts +12 -0
  18. package/dist/src/json-schema.js +202 -0
  19. package/dist/src/library.js +13 -1
  20. package/dist/src/notify.d.ts +79 -1
  21. package/dist/src/notify.js +243 -9
  22. package/dist/src/paths.d.ts +5 -0
  23. package/dist/src/paths.js +32 -0
  24. package/dist/src/platform.d.ts +112 -2
  25. package/dist/src/platform.js +20 -2
  26. package/dist/src/provider-health.d.ts +55 -0
  27. package/dist/src/provider-health.js +232 -0
  28. package/dist/src/run-container.d.ts +80 -4
  29. package/dist/src/run-container.js +176 -37
  30. package/dist/src/runner.d.ts +41 -5
  31. package/dist/src/runner.js +513 -81
  32. package/dist/src/script-tools.d.ts +12 -0
  33. package/dist/src/script-tools.js +10 -2
  34. package/dist/src/secret-health.d.ts +59 -0
  35. package/dist/src/secret-health.js +155 -0
  36. package/dist/src/secrets.d.ts +19 -1
  37. package/dist/src/secrets.js +44 -12
  38. package/dist/src/starter.js +4 -0
  39. package/dist/src/step-exec.d.ts +62 -6
  40. package/dist/src/step-exec.js +199 -68
  41. package/dist/src/store.d.ts +300 -3
  42. package/dist/src/store.js +574 -26
  43. package/dist/src/tar.js +2 -1
  44. package/dist/src/test-mode.d.ts +111 -0
  45. package/dist/src/test-mode.js +285 -0
  46. package/dist/src/tool-test.js +48 -21
  47. package/dist/src/translator.js +2 -1
  48. package/dist/src/trigger-log.d.ts +43 -0
  49. package/dist/src/trigger-log.js +122 -0
  50. package/dist/src/triggers.d.ts +20 -0
  51. package/dist/src/triggers.js +91 -10
  52. package/dist/src/webhook.d.ts +15 -3
  53. package/dist/src/webhook.js +11 -11
  54. package/package.json +6 -5
  55. package/src/approvals.ts +253 -2
  56. package/src/budget.ts +178 -0
  57. package/src/catalog.ts +2 -1
  58. package/src/confine.ts +6 -1
  59. package/src/deploy.ts +4 -1
  60. package/src/egress.ts +20 -4
  61. package/src/evals.ts +25 -9
  62. package/src/flow-lint.ts +118 -0
  63. package/src/host-env.ts +51 -0
  64. package/src/json-schema.ts +184 -0
  65. package/src/library.ts +13 -1
  66. package/src/notify.ts +272 -11
  67. package/src/paths.ts +29 -0
  68. package/src/platform.ts +119 -3
  69. package/src/provider-health.ts +274 -0
  70. package/src/run-container.ts +215 -35
  71. package/src/runner.ts +525 -73
  72. package/src/script-tools.ts +18 -2
  73. package/src/secret-health.ts +185 -0
  74. package/src/secrets.ts +44 -10
  75. package/src/starter.ts +4 -0
  76. package/src/step-exec.ts +176 -21
  77. package/src/storage.ts +8 -2
  78. package/src/store.ts +785 -25
  79. package/src/tar.ts +2 -1
  80. package/src/test-mode.ts +311 -0
  81. package/src/tool-test.ts +49 -20
  82. package/src/translator.ts +2 -1
  83. package/src/trigger-log.ts +159 -0
  84. package/src/triggers.ts +92 -10
  85. package/src/webhook.ts +17 -11
@@ -23,6 +23,7 @@ import path from "node:path";
23
23
  import crypto from "node:crypto";
24
24
  import { spawn, spawnSync } from "node:child_process";
25
25
  import { isPlatformPath } from "./store.js";
26
+ import { divertedSummary, isDivertedPath } from "./test-mode.js";
26
27
  import { isFileValue, fileContent } from "./secrets.js";
27
28
  import { safeTenantSegment } from "./runtime.js";
28
29
  import { dataRoot } from "./paths.js";
@@ -95,7 +96,69 @@ export function allowedBack(rel) {
95
96
  * the step did not touch it, so it is not ours to write back. Concurrent
96
97
  * edits survive, and a step that genuinely changed a file still wins.
97
98
  */
98
- export function applyContainerChanges(hostWs, containerWs, baseline) {
99
+ /**
100
+ * What a workspace copy looked like when it went in: the sha256 of every
101
+ * file the step could hand back. Small enough to ride into the sandbox
102
+ * beside the input, so a driver that inherits the step can tell what the
103
+ * step changed without the staging copy the first driver held in /tmp.
104
+ */
105
+ export function hashTree(dir) {
106
+ const out = {};
107
+ const walk = (d) => {
108
+ for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
109
+ const abs = path.join(d, entry.name);
110
+ const rel = path.relative(dir, abs).replaceAll("\\", "/");
111
+ if (entry.isSymbolicLink())
112
+ continue;
113
+ if (entry.isDirectory()) {
114
+ if (allowedBack(rel + "/"))
115
+ walk(abs);
116
+ continue;
117
+ }
118
+ if (!allowedBack(rel))
119
+ continue;
120
+ out[rel] = crypto.createHash("sha256").update(fs.readFileSync(abs)).digest("hex");
121
+ }
122
+ };
123
+ if (fs.existsSync(dir))
124
+ walk(dir);
125
+ return out;
126
+ }
127
+ const sha = (b) => crypto.createHash("sha256").update(b).digest("hex");
128
+ /**
129
+ * Both sides changed a file the step was handed. When each is the handed-in
130
+ * content with lines added on the end — a ledger, history.md, a .jsonl — the
131
+ * answer is the base, then the host's additions, then the step's. Only the
132
+ * base's hash may be known, so the base is found as the longest shared
133
+ * leading run of whole lines whose hash matches. Anything else is null: a
134
+ * real conflict, which the caller must not settle by dropping one side.
135
+ */
136
+ export function mergeAppends(was, host, next) {
137
+ // Both sides created the same new file: no shared base to append to, and
138
+ // joining two JSON documents is corruption, not a merge.
139
+ if (was === null)
140
+ return null;
141
+ const h = host.toString("utf8");
142
+ const n = next.toString("utf8");
143
+ let common = 0;
144
+ const max = Math.min(h.length, n.length);
145
+ while (common < max && h.charCodeAt(common) === n.charCodeAt(common))
146
+ common++;
147
+ for (let end = common; end >= 0; end--) {
148
+ if (end > 0 && h[end - 1] !== "\n")
149
+ continue;
150
+ const prefix = h.slice(0, end);
151
+ const matches = typeof was === "string" ? sha(Buffer.from(prefix, "utf8")) === was : Buffer.from(prefix, "utf8").equals(was);
152
+ if (!matches)
153
+ continue;
154
+ const hostAdded = h.slice(end);
155
+ const stepAdded = n.slice(end);
156
+ const joined = hostAdded && !hostAdded.endsWith("\n") ? hostAdded + "\n" : hostAdded;
157
+ return Buffer.from(prefix + joined + stepAdded, "utf8");
158
+ }
159
+ return null;
160
+ }
161
+ export function applyContainerChanges(hostWs, containerWs, baseline, note, divert) {
99
162
  const applied = [];
100
163
  const walk = (dir) => {
101
164
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
@@ -116,17 +179,61 @@ export function applyContainerChanges(hostWs, containerWs, baseline) {
116
179
  }
117
180
  if (!allowedBack(rel))
118
181
  continue;
119
- const next = fs.readFileSync(abs);
182
+ let next = fs.readFileSync(abs);
183
+ // What the step was handed: its bytes (a staging dir) or its hash.
184
+ // null when the file did not exist then; undefined when no baseline.
185
+ let was;
186
+ if (typeof baseline === "string") {
187
+ const p = path.join(baseline, rel);
188
+ was = fs.existsSync(p) ? fs.readFileSync(p) : null;
189
+ }
190
+ else if (baseline) {
191
+ was = baseline[rel.replaceAll("\\", "/")] ?? null;
192
+ }
193
+ const same = (b) => was === null || was === undefined ? false : typeof was === "string" ? sha(b) === was : b.equals(was);
120
194
  // Untouched by this step: whatever the host says now is more current
121
195
  // than what we handed in, including a change made while it ran.
122
- if (baseline) {
123
- const was = path.join(baseline, rel);
124
- if (fs.existsSync(was) && fs.readFileSync(was).equals(next))
196
+ if (same(next))
197
+ continue;
198
+ const target = path.join(hostWs, rel);
199
+ const hostNow = fs.existsSync(target) ? fs.readFileSync(target) : null;
200
+ if (hostNow && hostNow.equals(next))
201
+ continue;
202
+ // The host moved too since the step was handed its copy — another run
203
+ // or a person wrote it. Writing the step's copy over it erased the
204
+ // other run's ledger rows on 2026-09-14. Merge appends; keep both
205
+ // sides of anything else.
206
+ const hostMoved = was !== undefined && hostNow !== null && !same(hostNow);
207
+ if (hostMoved) {
208
+ const merged = mergeAppends(was ?? null, hostNow, next);
209
+ if (merged) {
210
+ next = merged;
211
+ }
212
+ else {
213
+ const ext = path.extname(rel);
214
+ const stem = rel.slice(0, rel.length - ext.length);
215
+ const conflictRel = `${stem}.conflict-${Date.now().toString(36)}${ext}`;
216
+ const conflictAbs = path.join(hostWs, conflictRel);
217
+ fs.mkdirSync(path.dirname(conflictAbs), { recursive: true });
218
+ fs.writeFileSync(conflictAbs, next);
219
+ applied.push(conflictRel);
220
+ note?.(`write-back conflict on ${rel.replaceAll("\\", "/")}: it changed on the workspace while this step ran, ` +
221
+ `and not only by appending — kept the workspace's version; this step's version is ${conflictRel.replaceAll("\\", "/")}`);
125
222
  continue;
223
+ }
126
224
  }
127
- const target = path.join(hostWs, rel);
128
- if (fs.existsSync(target) && fs.readFileSync(target).equals(next))
225
+ // A test run's state/ and storage/ writes land under the run, not on
226
+ // the workspace: the step ran against the real files and its output
227
+ // is kept where a person can read it, but the next real run starts
228
+ // from state a test never touched. Decided after the merge so the
229
+ // diverted copy is what WOULD have been written, conflicts included.
230
+ if (divert && isDivertedPath(rel)) {
231
+ const aside = path.join(divert.to, rel);
232
+ fs.mkdirSync(path.dirname(aside), { recursive: true });
233
+ fs.writeFileSync(aside, next);
234
+ divert.note(rel.replaceAll("\\", "/"), divertedSummary(rel.replaceAll("\\", "/"), hostNow, next));
129
235
  continue;
236
+ }
130
237
  fs.mkdirSync(path.dirname(target), { recursive: true });
131
238
  fs.writeFileSync(target, next);
132
239
  applied.push(rel);
@@ -253,9 +360,9 @@ try {
253
360
 
254
361
  // API and script tools, rebuilt in here from their specs. Secrets were
255
362
  // substituted into API headers before the input crossed the boundary, so
256
- // buildApiTools gets an already-resolved environment. (verify: needs no
257
- // separate env either — the container's own environment already carries
258
- // the declared secrets, which is why executeStep gets verifyEnv: {}.)
363
+ // buildApiTools gets an already-resolved environment. verify: gets the
364
+ // container's own environment, whole: it is the boundary, assembled on
365
+ // the host from an allowlist, and the declared secrets are in it.
259
366
  const api = buildApiTools("", input.apis, undefined, { env, missing: [] });
260
367
  const script = buildScriptTools(
261
368
  agentDir,
@@ -300,10 +407,13 @@ try {
300
407
  env,
301
408
  timeoutSec: input.timeoutSec,
302
409
  budgetUsd: input.budgetUsd,
410
+ budgetNote: input.budgetNote,
303
411
  price: input.price,
304
412
  verify: input.verify,
305
- verifyEnv: {},
413
+ verifyEnv: env,
306
414
  output: input.output,
415
+ schema: input.schema,
416
+ maxTurns: input.maxTurns,
307
417
  // The container is the boundary; the SDK's own bash sandbox here would
308
418
  // only block declared network use (SSH, curl) for no added safety.
309
419
  sandboxBash: false,
@@ -409,8 +519,17 @@ export function runnerImageTag() {
409
519
  * has not been built before. `npm pack` of the installed core, so the image
410
520
  * runs the same bytes the server does.
411
521
  */
412
- export function ensureRunnerImage() {
413
- const tag = process.env.FOLDRUN_RUNNER_IMAGE ?? runnerImageTag();
522
+ /**
523
+ * `platform`: build for another architecture — "linux/arm64" on the amd64
524
+ * box, through QEMU — and name the result with the arch appended so it never
525
+ * shadows the native image in the daemon. Only the publisher asks for this;
526
+ * a step always runs the native image. Needs binfmt registered on the host
527
+ * (tonistiigi/binfmt); the publisher does that itself, every time, because
528
+ * the registration does not survive a reboot.
529
+ */
530
+ export function ensureRunnerImage(opts = {}) {
531
+ const arch = opts.platform ? `-${opts.platform.split("/").pop()}` : "";
532
+ const tag = process.env.FOLDRUN_RUNNER_IMAGE ?? runnerImageTag() + arch;
414
533
  const log = [];
415
534
  if (process.env.FOLDRUN_RUNNER_IMAGE)
416
535
  return { tag, log };
@@ -430,7 +549,8 @@ export function ensureRunnerImage() {
430
549
  fs.writeFileSync(path.join(build, "entry.sh"), ENTRY);
431
550
  fs.writeFileSync(path.join(build, "Dockerfile"), DOCKERFILE);
432
551
  log.push(`building runner image ${tag} (first run only)`);
433
- const out = spawnSync(cli(), ["build", "-t", tag, build], { encoding: "utf8" });
552
+ const args = ["build", ...(opts.platform ? ["--platform", opts.platform] : []), "-t", tag, build];
553
+ const out = spawnSync(cli(), args, { encoding: "utf8" });
434
554
  if (out.status !== 0) {
435
555
  throw new Error(`runner image build failed:\n${(out.stderr || out.stdout).slice(-2000)}`);
436
556
  }
@@ -505,6 +625,44 @@ function ensureRunNetwork() {
505
625
  }
506
626
  return RUN_NETWORK;
507
627
  }
628
+ /**
629
+ * What crosses into the container as its environment, and how.
630
+ *
631
+ * A docker --env-file is one `KEY=value` per line and cannot carry a
632
+ * newline. @file secrets are multi-line by nature (a PEM key, a cert), so
633
+ * they are staged as files NOW, into the copied-in workspace, and the env
634
+ * carries the container path — the one place a path is stable across the
635
+ * boundary. A plain secret whose value happens to hold a newline gets the
636
+ * same treatment, and the trace says so: it used to be dropped from the
637
+ * env file without a word, and the step failed later, elsewhere, on a
638
+ * variable that read as unset. Everything else goes through the env file.
639
+ */
640
+ export function stageContainerEnv(env, wsIn, agentRel, emit) {
641
+ const containerEnv = {};
642
+ const stage = (k, content) => {
643
+ const rel = path.join(agentRel, ".secret-files", k.toLowerCase());
644
+ const abs = path.join(wsIn, rel);
645
+ fs.mkdirSync(path.dirname(abs), { recursive: true, mode: 0o700 });
646
+ fs.writeFileSync(abs, content, { mode: 0o600 });
647
+ return `/workspace/${rel.replaceAll("\\", "/")}`;
648
+ };
649
+ for (const [k, v] of Object.entries(env)) {
650
+ if (typeof v !== "string")
651
+ continue;
652
+ if (isFileValue(v)) {
653
+ containerEnv[k] = stage(k, fileContent(v));
654
+ }
655
+ else if (/[\r\n]/.test(v)) {
656
+ containerEnv[k] = stage(k, v);
657
+ emit("info", `${k} holds a line break and cannot cross as an environment variable — ` +
658
+ `passed as a file instead; $${k} is its path in the sandbox`);
659
+ }
660
+ else {
661
+ containerEnv[k] = v;
662
+ }
663
+ }
664
+ return containerEnv;
665
+ }
508
666
  /** The limits a size class reserves. Large is the install's configured
509
667
  * limits unchanged, so existing installs bill and behave identically;
510
668
  * small defaults to a quarter-ish slice and is env-tunable. */
@@ -595,6 +753,8 @@ export async function runStepInContainer(args) {
595
753
  filter: (src) => !isPlatformPath(path.relative(args.workspaceRoot, src)),
596
754
  });
597
755
  const libIn = path.join(staging, "library");
756
+ if (args.galleryRoot && fs.existsSync(args.galleryRoot))
757
+ fs.cpSync(args.galleryRoot, libIn, { recursive: true });
598
758
  if (fs.existsSync(args.libraryRoot))
599
759
  fs.cpSync(args.libraryRoot, libIn, { recursive: true });
600
760
  else
@@ -602,28 +762,7 @@ export async function runStepInContainer(args) {
602
762
  const jobIn = path.join(staging, "job");
603
763
  fs.mkdirSync(jobIn);
604
764
  fs.writeFileSync(path.join(jobIn, "input.json"), JSON.stringify(args.input, null, 2));
605
- // @file secrets are multi-line (a PEM key, a cert), and a docker
606
- // --env-file cannot carry a newline. So they are staged as files *now*,
607
- // into the copied-in workspace, and the env carries the container path —
608
- // the one place a path is stable across the boundary. Everything else
609
- // goes through the env file. (Without this the key silently vanished:
610
- // the old filter dropped any value with a newline, and ssh got an empty
611
- // -i path.)
612
- const containerEnv = {};
613
- for (const [k, v] of Object.entries(args.env)) {
614
- if (typeof v !== "string")
615
- continue;
616
- if (isFileValue(v)) {
617
- const rel = path.join(args.input.agentRel, ".secret-files", k.toLowerCase());
618
- const abs = path.join(wsIn, rel);
619
- fs.mkdirSync(path.dirname(abs), { recursive: true, mode: 0o700 });
620
- fs.writeFileSync(abs, fileContent(v), { mode: 0o600 });
621
- containerEnv[k] = `/workspace/${rel.replaceAll("\\", "/")}`;
622
- }
623
- else if (!v.includes("\n")) {
624
- containerEnv[k] = v;
625
- }
626
- }
765
+ const containerEnv = stageContainerEnv(args.env, wsIn, args.input.agentRel, args.emit);
627
766
  const envFile = path.join(staging, "env");
628
767
  fs.writeFileSync(envFile, Object.entries(containerEnv).map(([k, v]) => `${k}=${v}`).join("\n"), { mode: 0o600 });
629
768
  const flags = [
@@ -737,7 +876,7 @@ export async function runStepInContainer(args) {
737
876
  fs.mkdirSync(wsOut);
738
877
  const back = spawnSync(cli(), ["cp", `${containerId}:/workspace/.`, wsOut], { encoding: "utf8" });
739
878
  if (back.status === 0) {
740
- applyContainerChanges(args.workspaceRoot, wsOut, wsIn);
879
+ applyContainerChanges(args.workspaceRoot, wsOut, wsIn, (m) => args.emit("error", m), args.divert);
741
880
  }
742
881
  else {
743
882
  args.emit("error", `copy-out failed — the step's file changes were lost:\n${back.stderr.slice(0, 500)}`);
@@ -1,4 +1,4 @@
1
- import { type FlowStep, type RunRecord } from "./store.ts";
1
+ import { type FlowStep, type RunRecord, type StepRecord } from "./store.ts";
2
2
  /** How much of the earlier groups' output a step is handed, in characters.
3
3
  * Newest groups are kept when the total is over it. */
4
4
  export declare const CONTEXT_CHARS = 30000;
@@ -12,6 +12,17 @@ export declare const DESK_RUNS_CAP = 100;
12
12
  * step is most likely to be about.
13
13
  */
14
14
  export declare function joinEarlierGroups(groupResults: (string | null)[], gi: number, cap?: number): string | null;
15
+ /**
16
+ * `parallel: N` — a pool of N slots the instances of one fan-out share.
17
+ * Fan-out always ran every instance at once, and still does unless the
18
+ * step says otherwise: twenty browser steps against one site, or one
19
+ * vendor's rate limit, is what the cap is for. Nothing is rented while an
20
+ * instance waits for a slot.
21
+ */
22
+ export declare function slotPool(size: number): {
23
+ acquire: () => Promise<void>;
24
+ release: () => void;
25
+ };
15
26
  export interface DiscoveredSkill {
16
27
  name: string;
17
28
  description: string;
@@ -102,6 +113,20 @@ export declare function applicableSkills<T extends {
102
113
  */
103
114
  export declare function saysUntilMarker(result: string | null, until: string): boolean;
104
115
  export declare function resolveDocLinks(text: string, workspaceRoot: string): string;
116
+ /**
117
+ * Write one attempt onto the step's history and re-total the step.
118
+ *
119
+ * runStep reports the attempt it just ran — its cost, tokens and sandbox
120
+ * seconds — by assigning the step's fields. Those become this attempt's row;
121
+ * the step's fields then become the sum of every row, which is what the
122
+ * run's spend, the flow's budget: and the platform's bill read. A retried
123
+ * step used to carry only its last attempt's cost, so a `retry: 2` step
124
+ * could spend three shares while reporting one.
125
+ */
126
+ export declare function recordAttempt(step: StepRecord, n: number, status: "completed" | "failed", startedAt: string, eventsBefore: number,
127
+ /** The attempt re-attached to a sandbox a previous driver left: same
128
+ * attempt, so its row is replaced rather than repeated. */
129
+ reattach?: boolean): void;
105
130
  /**
106
131
  * Copy a directory tree by reading and writing the bytes.
107
132
  *
@@ -132,8 +157,19 @@ export declare const OVERLOAD_RETRIES = 3;
132
157
  */
133
158
  export declare function isTransientOverload(text: string): boolean;
134
159
  export declare function backoffMs(attempt: number, text?: string, random?: () => number): number;
135
- export declare function createFlowRun(tenant: string, workspace: string, steps: FlowStep[], flowName: string, status: "queued" | "running", tags?: string[]): RunRecord;
136
- export declare function startFlowRun(tenant: string, workspace: string, steps: FlowStep[], flowName: string, modelOverride?: string | null, tags?: string[], effortOverride?: string | null): RunRecord;
160
+ /**
161
+ * `schema: <path>` — the file, read and parsed, or null. Agent-relative like
162
+ * every other path in a flow (`../../schemas/lead.json`), confined to the
163
+ * workspace: a schema is the author's document, never something a step
164
+ * can point outside.
165
+ */
166
+ export declare function readSchemaFile(workspaceRoot: string, rel: string): Record<string, unknown> | boolean | null;
167
+ export declare function createFlowRun(tenant: string, workspace: string, steps: FlowStep[], flowName: string, status: "queued" | "running", tags?: string[], startedBy?: string | null, opts?: {
168
+ test?: boolean;
169
+ }): RunRecord;
170
+ export declare function startFlowRun(tenant: string, workspace: string, steps: FlowStep[], flowName: string, modelOverride?: string | null, tags?: string[], effortOverride?: string | null, startedBy?: string | null, opts?: {
171
+ test?: boolean;
172
+ }): RunRecord;
137
173
  /**
138
174
  * Drive a run to completion: groups in order, approval gates, retries.
139
175
  *
@@ -206,7 +242,7 @@ export interface Reconciliation {
206
242
  * outlives any process. A run whose person already answered is not waiting,
207
243
  * and is picked back up — see below.
208
244
  */
209
- export declare function reconcileRuns(tenant: string, now?: number): Reconciliation[];
245
+ export declare function reconcileRuns(tenant: string, now?: number, live?: ReadonlySet<string>): Reconciliation[];
210
246
  /**
211
247
  * The same, for every account this installation holds.
212
248
  *
@@ -214,7 +250,7 @@ export declare function reconcileRuns(tenant: string, now?: number): Reconciliat
214
250
  * one hard-coded account left every other account's interrupted runs marked
215
251
  * `running` forever, with no process left to finish them.
216
252
  */
217
- export declare function reconcileAllRuns(now?: number): Reconciliation[];
253
+ export declare function reconcileAllRuns(now?: number, live?: ReadonlySet<string>): Reconciliation[];
218
254
  /**
219
255
  * Wait for a run to stop being in progress.
220
256
  *