@foldrun/core 2.12.1 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/runner.ts CHANGED
@@ -19,7 +19,8 @@ import { cascadeLimits, toolOwners } from "./limits.ts";
19
19
  import { resolveAgentPath } from "./confine.ts";
20
20
  import { browserPodLine, podLossDecision, RERAN_ON_FULL } from "./browser-pod.ts";
21
21
  import { eventUrl } from "./webhook.ts";
22
- import { runStepInContainer, sizeLimits, killRunSandboxes, type StepTiming } from "./run-container.ts";
22
+ import { runStepInContainer, resolveResources, memoryGiB, killRunSandboxes, type StepTiming } from "./run-container.ts";
23
+ import { readResources, hasOverride } from "./resources.ts";
23
24
  import { hostSafeEnv, platformApiUrl } from "./host-env.ts";
24
25
  import { resolveModelCredential, credentialLine, accountModelBlock, isClaudeLoginToken, isModelEnvName, DECLARED_ENV_PREFIX } from "./model-credential.ts";
25
26
  import { parseJudge } from "./judge-verdict.ts";
@@ -1296,9 +1297,10 @@ export function agentContext(
1296
1297
  // is large — the install's configured limits — so existing agents change
1297
1298
  // nothing. `size: small` is the opt-in for agents that wait on APIs more
1298
1299
  // than they work; `size: heavy` for a rendered browser or real data in
1299
- // memory. Memory is a hard ceiling per class; CPU bursts uncapped and bills
1300
- // at the class rate, so heavy both may use more and costs more. The actuals
1301
- // line in any trace is the evidence for right-sizing.
1300
+ // memory. Memory and cores are hard ceilings per class and bill at the
1301
+ // class rate (cores capped since 26 Oct 2026), so heavy both may use more
1302
+ // and costs more. The actuals line in any trace — busy, throttled, peak —
1303
+ // is the evidence for right-sizing.
1302
1304
  const size: "small" | "large" | "heavy" =
1303
1305
  front.size === "small" ? "small" : front.size === "heavy" ? "heavy" : "large";
1304
1306
 
@@ -2311,6 +2313,16 @@ async function runStep(
2311
2313
  // A retry that moved the step up a class (an evicted or OOM-killed
2312
2314
  // attempt) wins over the agent's own `size:` for the attempts after.
2313
2315
  const size = step.sizeUp ?? agentSize;
2316
+ // `resources:` over the class's figures, key by key. A value check would
2317
+ // refuse is dropped here too (the class's figure holds) and said so, so
2318
+ // `foldrun check` and the sandbox never disagree. After a retry moved up
2319
+ // a class, an override never holds the step below the new class.
2320
+ const asked = readResources(front.resources);
2321
+ for (const p of asked.problems) push("error", `resources: ${p}`);
2322
+ const limits = resolveResources(size, asked.value, { atLeastClass: !!step.sizeUp });
2323
+ // What the sandbox holds while it exists — `step.holding`, what the
2324
+ // account's compute in use sums (Settings, `foldrun usage compute`).
2325
+ const held = { cpus: Number(limits.cpus) || 2, memGiB: memoryGiB(limits.memory) };
2314
2326
 
2315
2327
  // oauth2 secrets become live access tokens here — the one async moment
2316
2328
  // in secret resolution, at the last host-side point before anything is
@@ -2345,7 +2357,10 @@ async function runStep(
2345
2357
  const noKey = !providerLabel && !platform.platformKeyAllowed(tenant);
2346
2358
  const credential = providerLabel || noKey ? null : resolveModelCredential(isolatedRun() ? platformModelEnv() : process.env);
2347
2359
  step.credential = credential ? credential.label : noKey ? "none — this account has no model key" : `provider: ${providerLabel}`;
2348
- if (credential) {
2360
+ // Core's own Docker container has no login to fall back to (below):
2361
+ // there, "none set" is refused with what to do, not described.
2362
+ const noCredentialInContainer = credential?.kind === "none" && process.env.FOLDRUN_RUN_ISOLATION === "container";
2363
+ if (credential && !noCredentialInContainer) {
2349
2364
  const line = credentialLine(credential);
2350
2365
  push(line.type, line.text);
2351
2366
  }
@@ -2372,6 +2387,24 @@ async function runStep(
2372
2387
  return;
2373
2388
  }
2374
2389
  }
2390
+ // A step in core's own Docker container (self-hosting) has no machine
2391
+ // login to fall back to: with no credential at all it could only fail at
2392
+ // the model, with Claude Code's own "Not logged in · Please run /login".
2393
+ // Refused here, before a sandbox is rented, saying what to set (a
2394
+ // self-hosted install with no key, 26 Oct 2026). A local `foldrun run`
2395
+ // keeps the machine's login; a registered executor brings its own.
2396
+ if (noCredentialInContainer) {
2397
+ push(
2398
+ "error",
2399
+ "no model credential: this install has none and the account has no key of its own. Set ANTHROPIC_API_KEY " +
2400
+ "(or CLAUDE_CODE_OAUTH_TOKEN from `claude setup-token`) in the platform's environment — for Docker Compose, " +
2401
+ "self-hosting/.env, then `docker compose up -d` — or add the account's key in Settings → Model " +
2402
+ "(`foldrun account model set <provider> --key …`)",
2403
+ );
2404
+ step.status = "failed";
2405
+ save();
2406
+ return;
2407
+ }
2375
2408
  for (const w of providerWarnings) push("error", w);
2376
2409
  // A level that wrote a language nobody can read was skipped, not obeyed.
2377
2410
  for (const l of language.lines) push("info", l);
@@ -3080,6 +3113,7 @@ async function runStep(
3080
3113
  // executor asks this before it creates one (pod-room.ts).
3081
3114
  ...(stopRequested ? { stopRequested } : {}),
3082
3115
  size,
3116
+ limits,
3083
3117
  // The pod's network, for the executor's own wall (the k8s tier's
3084
3118
  // network policy): a filtered pod reaches the proxy and DNS only.
3085
3119
  network: { level: network.level, enforced: forward?.enforce === true },
@@ -3096,6 +3130,8 @@ async function runStep(
3096
3130
  // saves the record, so this only updates the fields.
3097
3131
  checkpoint: (ref: string, consumed: number) => {
3098
3132
  step.sandbox = { kind: isolation!, ref, consumed, since: step.sandbox?.since ?? new Date().toISOString() };
3133
+ // The sandbox exists: past the room gate, it now holds its figures.
3134
+ if (consumed === 0) step.holding = held;
3099
3135
  if (consumed === 0) save();
3100
3136
  },
3101
3137
  });
@@ -3111,9 +3147,23 @@ async function runStep(
3111
3147
  // The arguments of the attempt whose outcome is current — what a
3112
3148
  // re-run on the full image repeats, on the same model supply.
3113
3149
  let lastArgs: typeof firstArgs = firstArgs;
3114
- const runTracked = (a: typeof firstArgs) => {
3150
+ const runTracked = async (a: typeof firstArgs) => {
3115
3151
  lastArgs = a;
3116
- return runIsolated(a);
3152
+ // An executor that cannot report its sandbox (Docker: no room gate,
3153
+ // it starts at once) holds from the call; one that can says so at
3154
+ // its first checkpoint, after any wait for room.
3155
+ if (!platform.sandboxResumable(isolation!)) {
3156
+ step.holding = held;
3157
+ save();
3158
+ }
3159
+ try {
3160
+ return await runIsolated(a);
3161
+ } finally {
3162
+ // Saved now: the clean-up after an attempt (the log drain, the
3163
+ // lease, publishing) can take a while, and the sandbox is gone.
3164
+ step.holding = null;
3165
+ save();
3166
+ }
3117
3167
  };
3118
3168
  let attemptFrom = step.events.length;
3119
3169
  let outcome = await runTracked(firstArgs);
@@ -3306,6 +3356,7 @@ async function runStep(
3306
3356
  push(
3307
3357
  "info",
3308
3358
  `resources: cpu busy ${outcome.res.busyCpuSecs?.toFixed(1) ?? "?"}s · ` +
3359
+ (outcome.res.throttledSecs ? `throttled ${outcome.res.throttledSecs.toFixed(1)}s · ` : "") +
3309
3360
  `peak mem ${mb(outcome.res.peakMemBytes)}MB · ` +
3310
3361
  `net ${mb(outcome.res.rxBytes)}MB in / ${mb(outcome.res.txBytes)}MB out`,
3311
3362
  );
@@ -3326,13 +3377,8 @@ async function runStep(
3326
3377
  // is per core-second and per GiB-second, and what was held must come
3327
3378
  // from the record, never from what the config says at billing time.
3328
3379
  step.size = size;
3329
- const lim = sizeLimits(size);
3330
- const gib = (() => {
3331
- const m = lim.memory.match(/^(\d+(?:\.\d+)?)\s*(Gi?|Mi?|g|m)?/i);
3332
- const n = m ? Number(m[1]) : 2;
3333
- return (m?.[2] ?? "Gi").toLowerCase().startsWith("m") ? n / 1024 : n;
3334
- })();
3335
- step.reserved = { cpus: Number(lim.cpus) || 2, memGiB: gib };
3380
+ if (hasOverride(asked.value)) step.resources = asked.value;
3381
+ step.reserved = held;
3336
3382
  } else {
3337
3383
  // Same withholding as the sandbox path — the in-process step is what
3338
3384
  // `foldrun run --test` drives on a laptop, where there is no proxy
package/src/starter.ts CHANGED
@@ -490,6 +490,7 @@ owner: ana@example.com # who answers for it; else the workspace's, else the
490
490
  model: default # fast | default | max
491
491
  effort: low # how hard to think about it
492
492
  size: large # small | large | heavy — the sandbox it rents
493
+ resources: { memory: 4Gi } # optional: cpu, memory, disk in numbers, over the size class
493
494
  tools: [web, read, ask, peers, my-folder-tool] # built-ins and your own tools/, one list; \`ask\` lets it ask you mid-step, \`peers\` lets it message the steps running beside it
494
495
  disallowedTools: [Bash] # subtract from what it would otherwise have
495
496
  skills: [house-style] # from skills/
package/src/store.ts CHANGED
@@ -3533,6 +3533,14 @@ export interface StepRecord {
3533
3533
  * from before sizes existed — billing falls back to the flat rate. */
3534
3534
  size?: "small" | "large" | "heavy";
3535
3535
  reserved?: { cpus: number; memGiB: number } | null;
3536
+ /** The agent's `resources:` as applied (resources.ts), when it set any:
3537
+ * why `reserved` differs from the class's figures. Absent before 26 Oct
3538
+ * 2026 and on steps of an agent that wrote none. */
3539
+ resources?: { cpu?: number; memory?: string; disk?: string };
3540
+ /** What the step's sandbox holds while it exists — set once it is past
3541
+ * the room gate, cleared when the attempt ends. What an account's
3542
+ * compute in use sums; `reserved` is the bill's record of the same. */
3543
+ holding?: { cpus: number; memGiB: number } | null;
3536
3544
  /** Token counts behind costUsd, kept so usage reports can say "how many"
3537
3545
  * and not only "how much" — the price of a token changes, the count is
3538
3546
  * the fact. Absent on steps recorded before this existed. */
@@ -3557,6 +3565,9 @@ export interface StepRecord {
3557
3565
  actual?: {
3558
3566
  busyCpuSecs: number | null;
3559
3567
  peakMemBytes: number | null;
3568
+ /** Seconds the CPU cap (the class's cores) held the sandbox back, from
3569
+ * cpu.stat. Absent before 26 Oct 2026. */
3570
+ throttledSecs?: number | null;
3560
3571
  rxBytes: number | null;
3561
3572
  txBytes: number | null;
3562
3573
  } | null;