@foldrun/core 1.4.2 → 2.0.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/dist/index.d.ts +0 -3
- package/dist/index.js +0 -3
- package/dist/src/deploy.d.ts +0 -26
- package/dist/src/deploy.js +7 -52
- package/dist/src/history.d.ts +30 -10
- package/dist/src/history.js +232 -99
- package/dist/src/library.d.ts +0 -13
- package/dist/src/library.js +0 -50
- package/dist/src/platform.d.ts +1 -3
- package/dist/src/platform.js +0 -1
- package/dist/src/run-container.d.ts +29 -0
- package/dist/src/run-container.js +63 -2
- package/dist/src/runner.js +10 -2
- package/dist/src/secrets.js +1 -12
- package/dist/src/starter.js +2 -2
- package/dist/src/store.d.ts +15 -10
- package/dist/src/store.js +45 -21
- package/index.ts +0 -3
- package/package.json +1 -3
- package/src/deploy.ts +2 -70
- package/src/history.ts +230 -108
- package/src/library.ts +0 -44
- package/src/platform.ts +1 -4
- package/src/run-container.ts +68 -2
- package/src/runner.ts +10 -1
- package/src/secrets.ts +1 -11
- package/src/starter.ts +2 -2
- package/src/store.ts +55 -27
- package/dist/src/git-hook.d.ts +0 -1
- package/dist/src/git-hook.js +0 -70
- package/dist/src/git.d.ts +0 -37
- package/dist/src/git.js +0 -101
- package/dist/src/gitrepo.d.ts +0 -113
- package/dist/src/gitrepo.js +0 -404
- package/dist/src/tar.d.ts +0 -30
- package/dist/src/tar.js +0 -112
- package/src/git-hook.ts +0 -73
- package/src/git.ts +0 -117
- package/src/gitrepo.ts +0 -439
- package/src/tar.ts +0 -125
package/dist/src/library.js
CHANGED
|
@@ -344,53 +344,3 @@ registerTreeReader((tenant, scope) => {
|
|
|
344
344
|
}
|
|
345
345
|
return out;
|
|
346
346
|
});
|
|
347
|
-
/**
|
|
348
|
-
* Make the shelf match a tree — what a push to the library repository does.
|
|
349
|
-
* Written with plain fs and recorded as ONE revision at the end: going
|
|
350
|
-
* through writeLibraryFile per file would commit each one on top of the
|
|
351
|
-
* pushed commit, and the history would show a push as fifty edits.
|
|
352
|
-
*/
|
|
353
|
-
export function syncLibraryFromTree(tenant, files, meta = {}) {
|
|
354
|
-
const wanted = new Map();
|
|
355
|
-
for (const f of files) {
|
|
356
|
-
const [kind, ...rest] = f.path.split("/");
|
|
357
|
-
if (!LIBRARY_KINDS.includes(kind) || rest.length === 0)
|
|
358
|
-
continue;
|
|
359
|
-
wanted.set(f.path, f.content);
|
|
360
|
-
}
|
|
361
|
-
const changes = [];
|
|
362
|
-
for (const kind of LIBRARY_KINDS) {
|
|
363
|
-
const dir = libraryDir(tenant, kind);
|
|
364
|
-
if (!fs.existsSync(dir))
|
|
365
|
-
continue;
|
|
366
|
-
for (const entry of fs.readdirSync(dir, { recursive: true })) {
|
|
367
|
-
const rel = `${kind}/${String(entry).split(path.sep).join("/")}`;
|
|
368
|
-
const full = path.join(dir, String(entry));
|
|
369
|
-
try {
|
|
370
|
-
if (!fs.statSync(full).isFile() || !TEXT_EXT.test(rel))
|
|
371
|
-
continue;
|
|
372
|
-
}
|
|
373
|
-
catch {
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
if (!wanted.has(rel)) {
|
|
377
|
-
changes.push({ path: rel, before: fs.readFileSync(full, "utf8"), after: null });
|
|
378
|
-
fs.rmSync(full, { force: true });
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
for (const [rel, content] of wanted) {
|
|
383
|
-
const [kind, ...rest] = rel.split("/");
|
|
384
|
-
const full = path.join(libraryDir(tenant, kind), assertLibraryPath(kind, rest.join("/")));
|
|
385
|
-
const before = fs.existsSync(full) ? fs.readFileSync(full, "utf8") : null;
|
|
386
|
-
if (before === content)
|
|
387
|
-
continue;
|
|
388
|
-
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
389
|
-
fs.writeFileSync(full, content);
|
|
390
|
-
if (kind === "scripts" || /scripts\//.test(rel) || (kind === "tools" && !rel.endsWith(".md")))
|
|
391
|
-
fs.chmodSync(full, 0o755);
|
|
392
|
-
changes.push({ path: rel, before, after: content });
|
|
393
|
-
}
|
|
394
|
-
recordRevision(tenant, "@library", changes, { by: meta.by ?? "deploy", message: meta.commit ? `deployed ${meta.commit.slice(0, 7)}` : "synced from push", commit: meta.commit ?? null });
|
|
395
|
-
return changes.length;
|
|
396
|
-
}
|
package/dist/src/platform.d.ts
CHANGED
|
@@ -90,8 +90,6 @@ export interface PlatformHooks {
|
|
|
90
90
|
syncPublicShares(tenant: string, workspace: string): Promise<{
|
|
91
91
|
added: string[];
|
|
92
92
|
}>;
|
|
93
|
-
/** The workspace a preview was branched from, for inherited secrets. Default: none. */
|
|
94
|
-
previewSourceOf(tenant: string, workspace: string): string | null;
|
|
95
93
|
/** This account's own data key, or null to use the install key. */
|
|
96
94
|
tenantKey(tenant: string): Buffer | null;
|
|
97
95
|
/** The egress proxy — a per-step lease that keeps credentials out of the
|
|
@@ -101,7 +99,7 @@ export interface PlatformHooks {
|
|
|
101
99
|
* workspace deleted. What a scheduler that keeps flows in memory rather
|
|
102
100
|
* than re-reading every workspace each tick needs to hear. Default:
|
|
103
101
|
* nothing — locally the scheduler reads the disk. Never throws. */
|
|
104
|
-
workspaceChanged(tenant: string, workspace: string, why: "write" | "deploy" | "
|
|
102
|
+
workspaceChanged(tenant: string, workspace: string, why: "write" | "deploy" | "delete"): void;
|
|
105
103
|
/** Can a step in this isolation mode be re-attached to after the driver
|
|
106
104
|
* that started it is gone? True means an orphaned step with a `sandbox`
|
|
107
105
|
* on its record is resumed rather than destroyed and run again. Default:
|
package/dist/src/platform.js
CHANGED
|
@@ -97,7 +97,36 @@ export interface StepTiming {
|
|
|
97
97
|
sandboxMs: number;
|
|
98
98
|
firstOutputMs: number | null;
|
|
99
99
|
totalMs: number;
|
|
100
|
+
/** Where the start-up went, phase by phase — see STARTUP_PHASES. Absent
|
|
101
|
+
* from an executor that marks none, and from a resumed driver (the
|
|
102
|
+
* first driver's phases are on the record already). */
|
|
103
|
+
phases?: StartupPhases;
|
|
100
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* The start-up of one step, in the order it happens. Each is the moment the
|
|
107
|
+
* phase ENDED, in ms since the executor began the step, so the gap between
|
|
108
|
+
* two neighbours is what the later one cost (FOL-29: "pod ready" was the
|
|
109
|
+
* only number, and the six seconds after it had no names).
|
|
110
|
+
*
|
|
111
|
+
* The executor marks its own: `services` (the account's search/browser pods
|
|
112
|
+
* up), `room` (room on the cluster to start — platform pod-room.ts),
|
|
113
|
+
* `applied` (pod created), `ready`, `files` (the workspace copied in),
|
|
114
|
+
* `go` (the step released). The sandbox marks the rest as `{"e":"mark"}`
|
|
115
|
+
* lines: `shim` (the shim saw go), `system` (system packages restored),
|
|
116
|
+
* `driver` (node up), `imports` (core loaded), `tools` (runtime and tools
|
|
117
|
+
* built, the SDK about to start) and `model` (the model's first text or tool
|
|
118
|
+
* call). A worker that predates a mark ignores the line — parseDriverLine
|
|
119
|
+
* answers null for an `e` it does not know.
|
|
120
|
+
*/
|
|
121
|
+
export declare const STARTUP_PHASES: readonly ["services", "room", "applied", "ready", "files", "go", "shim", "system", "driver", "imports", "tools", "model"];
|
|
122
|
+
export type StartupPhase = (typeof STARTUP_PHASES)[number];
|
|
123
|
+
export type StartupPhases = Partial<Record<StartupPhase, number>>;
|
|
124
|
+
/** A sandbox-side mark: which phase ended, and when (epoch ms, the
|
|
125
|
+
* sandbox's clock — the same machine's as the worker's on one node). */
|
|
126
|
+
export declare function parseDriverMark(line: string): {
|
|
127
|
+
name: StartupPhase;
|
|
128
|
+
at: number;
|
|
129
|
+
} | null;
|
|
101
130
|
export interface StepActuals {
|
|
102
131
|
busyCpuSecs: number | null;
|
|
103
132
|
peakMemBytes: number | null;
|
|
@@ -30,6 +30,42 @@ import { safeTenantSegment, systemInstallSh, SYSTEM_FROM_INPUT_SH, INSTALL_SOCKE
|
|
|
30
30
|
import { egressCaSh } from "./network.js";
|
|
31
31
|
import { isOperatorEvent } from "./operator.js";
|
|
32
32
|
import { parseBrowserPod } from "./browser-pod.js";
|
|
33
|
+
/**
|
|
34
|
+
* The start-up of one step, in the order it happens. Each is the moment the
|
|
35
|
+
* phase ENDED, in ms since the executor began the step, so the gap between
|
|
36
|
+
* two neighbours is what the later one cost (FOL-29: "pod ready" was the
|
|
37
|
+
* only number, and the six seconds after it had no names).
|
|
38
|
+
*
|
|
39
|
+
* The executor marks its own: `services` (the account's search/browser pods
|
|
40
|
+
* up), `room` (room on the cluster to start — platform pod-room.ts),
|
|
41
|
+
* `applied` (pod created), `ready`, `files` (the workspace copied in),
|
|
42
|
+
* `go` (the step released). The sandbox marks the rest as `{"e":"mark"}`
|
|
43
|
+
* lines: `shim` (the shim saw go), `system` (system packages restored),
|
|
44
|
+
* `driver` (node up), `imports` (core loaded), `tools` (runtime and tools
|
|
45
|
+
* built, the SDK about to start) and `model` (the model's first text or tool
|
|
46
|
+
* call). A worker that predates a mark ignores the line — parseDriverLine
|
|
47
|
+
* answers null for an `e` it does not know.
|
|
48
|
+
*/
|
|
49
|
+
export const STARTUP_PHASES = [
|
|
50
|
+
"services", "room", "applied", "ready", "files", "go",
|
|
51
|
+
"shim", "system", "driver", "imports", "tools", "model",
|
|
52
|
+
];
|
|
53
|
+
/** A sandbox-side mark: which phase ended, and when (epoch ms, the
|
|
54
|
+
* sandbox's clock — the same machine's as the worker's on one node). */
|
|
55
|
+
export function parseDriverMark(line) {
|
|
56
|
+
const trimmed = line.trim();
|
|
57
|
+
if (!trimmed.startsWith('{"e":"mark"'))
|
|
58
|
+
return null;
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(trimmed);
|
|
61
|
+
if (parsed.e !== "mark" || typeof parsed.at !== "number" || !Number.isFinite(parsed.at))
|
|
62
|
+
return null;
|
|
63
|
+
return STARTUP_PHASES.includes(parsed.name) ? { name: parsed.name, at: parsed.at } : null;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
33
69
|
/**
|
|
34
70
|
* The one place the timing line is worded, so the docker and k8s traces
|
|
35
71
|
* read the same and a cold-start regression is greppable across both.
|
|
@@ -37,7 +73,11 @@ import { parseBrowserPod } from "./browser-pod.js";
|
|
|
37
73
|
export function timingLine(t) {
|
|
38
74
|
const s = (ms) => `${(ms / 1000).toFixed(2)}s`;
|
|
39
75
|
const first = t.firstOutputMs === null ? "no output" : `first output +${s(t.firstOutputMs)}`;
|
|
40
|
-
|
|
76
|
+
const phases = t.phases
|
|
77
|
+
? STARTUP_PHASES.filter((p) => typeof t.phases[p] === "number").map((p) => `${p} ${s(t.phases[p])}`)
|
|
78
|
+
: [];
|
|
79
|
+
return `timing: sandbox ready in ${s(t.sandboxMs)}, ${first}, ${s(t.totalMs)} total` +
|
|
80
|
+
(phases.length ? ` · start-up: ${phases.join(" → ")}` : "");
|
|
41
81
|
}
|
|
42
82
|
const cli = () => process.env.FOLDRUN_CONTAINER_CLI ?? "docker";
|
|
43
83
|
// Paths that must never come back from a container, whatever happened in
|
|
@@ -374,7 +414,17 @@ const DRIVER = `// The runner container's entrypoint. Reads one step, executes i
|
|
|
374
414
|
// same core the server uses, speaks JSONL on stdout, exits.
|
|
375
415
|
import fs from "node:fs";
|
|
376
416
|
const input = JSON.parse(fs.readFileSync("/opt/runner/job/input.json", "utf8"));
|
|
377
|
-
|
|
417
|
+
// Start-up marks (run-container.ts#STARTUP_PHASES): which phase just ended,
|
|
418
|
+
// on the sandbox's clock. A worker that does not know a mark skips the line.
|
|
419
|
+
const mark = (name) => process.stdout.write(JSON.stringify({ e: "mark", name, at: Date.now() }) + "\\n");
|
|
420
|
+
mark("driver");
|
|
421
|
+
let modelSpoke = false;
|
|
422
|
+
const emit = (type, text, extra) => {
|
|
423
|
+
// The model's first text or tool call is where start-up ends and the
|
|
424
|
+
// step's own work begins — runtime and tool lines before it are set-up.
|
|
425
|
+
if (!modelSpoke && (type === "text" || type === "tool")) { modelSpoke = true; mark("model"); }
|
|
426
|
+
process.stdout.write(JSON.stringify({ e: "event", type, text, ...extra }) + "\\n");
|
|
427
|
+
};
|
|
378
428
|
// What this sandbox actually touched, read at the last moment from the
|
|
379
429
|
// kernel's own books: cgroup v2 for CPU busy-time and peak memory,
|
|
380
430
|
// /proc/net/dev for bytes on the wire. Every read is best-effort — gVisor
|
|
@@ -430,6 +480,7 @@ try {
|
|
|
430
480
|
const { prepareRuntime } = await import("@foldrun/core/runtime");
|
|
431
481
|
const { materializeFileSecrets } = await import("@foldrun/core/secret-files");
|
|
432
482
|
const { paidWebActions } = await import("@foldrun/core/browser-pod");
|
|
483
|
+
mark("imports");
|
|
433
484
|
|
|
434
485
|
const agentDir = "/workspace/" + input.agentRel;
|
|
435
486
|
// HOME is where a bare relative path lands when anything resolves one
|
|
@@ -512,6 +563,7 @@ try {
|
|
|
512
563
|
const ask = input.ask ? buildAskTool({ channel, timeoutSec: input.ask.timeoutSec, emit }) : { server: null };
|
|
513
564
|
const inbox = channel ? inboxReader(channel, emit) : undefined;
|
|
514
565
|
|
|
566
|
+
mark("tools");
|
|
515
567
|
const outcome = await executeStep({
|
|
516
568
|
agentDir,
|
|
517
569
|
workspaceRoot: "/workspace",
|
|
@@ -1260,6 +1312,8 @@ export async function runStepInContainer(args) {
|
|
|
1260
1312
|
throw new Error(`container create failed:\n${create.stderr}`);
|
|
1261
1313
|
containerId = create.stdout.trim();
|
|
1262
1314
|
const sandboxMs = Date.now() - t0;
|
|
1315
|
+
// Docker's phases are measured from create, its rent clock.
|
|
1316
|
+
const phases = { ready: sandboxMs };
|
|
1263
1317
|
const cp = (from, to) => {
|
|
1264
1318
|
const out = spawnSync(cli(), ["cp", from, to], { encoding: "utf8" });
|
|
1265
1319
|
if (out.status !== 0)
|
|
@@ -1268,6 +1322,7 @@ export async function runStepInContainer(args) {
|
|
|
1268
1322
|
cp(`${wsIn}/.`, `${containerId}:/workspace`);
|
|
1269
1323
|
cp(`${libIn}/.`, `${containerId}:/library`);
|
|
1270
1324
|
cp(`${jobIn}/input.json`, `${containerId}:/opt/runner/job/input.json`);
|
|
1325
|
+
phases.files = Date.now() - t0;
|
|
1271
1326
|
// Start attached and read the driver's JSONL. The step timeout gets a
|
|
1272
1327
|
// backstop out here — a wedged container is killed, not waited on.
|
|
1273
1328
|
let firstOutputAt = null;
|
|
@@ -1292,6 +1347,11 @@ export async function runStepInContainer(args) {
|
|
|
1292
1347
|
break;
|
|
1293
1348
|
const line = buffer.slice(0, nl);
|
|
1294
1349
|
buffer = buffer.slice(nl + 1);
|
|
1350
|
+
const marked = parseDriverMark(line);
|
|
1351
|
+
if (marked) {
|
|
1352
|
+
phases[marked.name] ??= Math.max(0, marked.at - t0);
|
|
1353
|
+
continue;
|
|
1354
|
+
}
|
|
1295
1355
|
const parsed = parseDriverLine(line);
|
|
1296
1356
|
if (!parsed)
|
|
1297
1357
|
continue;
|
|
@@ -1339,6 +1399,7 @@ export async function runStepInContainer(args) {
|
|
|
1339
1399
|
sandboxMs,
|
|
1340
1400
|
firstOutputMs: firstOutputAt === null ? null : firstOutputAt - t0 - sandboxMs,
|
|
1341
1401
|
totalMs: Date.now() - t0,
|
|
1402
|
+
phases,
|
|
1342
1403
|
};
|
|
1343
1404
|
args.emit("info", timingLine(timing));
|
|
1344
1405
|
return { ...outcome, timing };
|
package/dist/src/runner.js
CHANGED
|
@@ -33,7 +33,7 @@ import { ASK_TOOL, applyOperatorEvent, askPromptLine, askTimeoutSec, buildAskToo
|
|
|
33
33
|
import { sendPlainNotification } from "./notify.js";
|
|
34
34
|
import { TOOL_MAP, BUILTIN_TOOLS, ownToolNames, toolRefs, legacyUseNames, legacyUseError, retiredToolNames, retiredToolError } from "./tool-names.js";
|
|
35
35
|
import { refNames } from "./refs.js";
|
|
36
|
-
import { accountDir, workspaceDir, buildMemoryIndex, workspaceMemoryIndex, knowledgeIndex, workspaceKnowledgeIndex, workspaceTools, brokenToolReport, listWorkspaces, listTenants, listRuns, syncBundleFor, parseToolDef, readFlow, markerPresent, whenRowsPath, parseApis, resolveModel, parseProvider, providerEnvFor, resolveEffort, isEffortWord, EFFORT_LEVELS, checkFormatVersion, readRun, writeRun, runSummary, verdictOf, sumAttempts, parseSchemaText, STORAGE_DIR, adoptLegacyVersionKey, listAgents, } from "./store.js";
|
|
36
|
+
import { accountDir, workspaceDir, buildMemoryIndex, workspaceMemoryIndex, knowledgeIndex, workspaceKnowledgeIndex, workspaceTools, brokenToolReport, listWorkspaces, listTenants, listRuns, liveRuns, syncBundleFor, parseToolDef, readFlow, markerPresent, whenRowsPath, parseApis, resolveModel, parseProvider, providerEnvFor, resolveEffort, isEffortWord, EFFORT_LEVELS, checkFormatVersion, readRun, writeRun, runSummary, verdictOf, sumAttempts, parseSchemaText, STORAGE_DIR, adoptLegacyVersionKey, listAgents, } from "./store.js";
|
|
37
37
|
import { loadCatalog, checkModel, clampEffort, catalogCost, findModel } from "./catalog.js";
|
|
38
38
|
import { resolveSecrets, getSecret, materializeSecrets, secretSites } from "./secrets.js";
|
|
39
39
|
import { resolveNetwork, networkLine, proxyEnv, EGRESS_CA_ENV } from "./network.js";
|
|
@@ -2687,6 +2687,12 @@ stepIndex) {
|
|
|
2687
2687
|
const secs = (ms) => Math.round(ms) / 1000;
|
|
2688
2688
|
step.computeSecs = outcome.timing ? secs(outcome.timing.totalMs) : null;
|
|
2689
2689
|
step.startupSecs = outcome.timing ? secs(outcome.timing.sandboxMs) : null;
|
|
2690
|
+
// Where the start-up went, phase by phase (run-container.ts#STARTUP_PHASES),
|
|
2691
|
+
// in seconds — what the cold-start numbers are computed from.
|
|
2692
|
+
const phases = outcome.timing?.phases;
|
|
2693
|
+
step.startup = phases && Object.keys(phases).length
|
|
2694
|
+
? Object.fromEntries(Object.entries(phases).map(([k, v]) => [k, secs(v)]))
|
|
2695
|
+
: null;
|
|
2690
2696
|
// Which reservation those seconds held — as numbers, because the bill
|
|
2691
2697
|
// is per core-second and per GiB-second, and what was held must come
|
|
2692
2698
|
// from the record, never from what the config says at billing time.
|
|
@@ -4768,7 +4774,9 @@ function lastActivity(run) {
|
|
|
4768
4774
|
export function reconcileRuns(tenant, now = Date.now(), live) {
|
|
4769
4775
|
const closed = [];
|
|
4770
4776
|
for (const workspace of listWorkspaces(tenant)) {
|
|
4771
|
-
|
|
4777
|
+
// Live runs only, read whole — every other status is passed over below,
|
|
4778
|
+
// and parsing the workspace's whole history every sweep was the cost.
|
|
4779
|
+
for (const summary of liveRuns(tenant, workspace.name)) {
|
|
4772
4780
|
if (drivingRuns.has(summary.id))
|
|
4773
4781
|
continue;
|
|
4774
4782
|
// Driven by another process — a second worker pod, on a platform with
|
package/dist/src/secrets.js
CHANGED
|
@@ -218,11 +218,9 @@ export function setSecretSites(tenant, name, sites, workspace) {
|
|
|
218
218
|
export function secretSites(tenant, names, workspace) {
|
|
219
219
|
const out = {};
|
|
220
220
|
const own = workspace ? read(tenant, workspace) : {};
|
|
221
|
-
const source = workspace ? platform.previewSourceOf(tenant, workspace) : null;
|
|
222
|
-
const inherited = source ? read(tenant, source) : {};
|
|
223
221
|
const account = read(tenant);
|
|
224
222
|
for (const name of names) {
|
|
225
|
-
const rec = own[name] ??
|
|
223
|
+
const rec = own[name] ?? account[name];
|
|
226
224
|
if (rec?.sites?.length)
|
|
227
225
|
out[name] = [...rec.sites];
|
|
228
226
|
}
|
|
@@ -298,15 +296,6 @@ export function getSecret(tenant, name, workspace) {
|
|
|
298
296
|
const own = decrypt(tenant, read(tenant, workspace)[name]);
|
|
299
297
|
if (own !== null)
|
|
300
298
|
return { value: own, scope: "workspace" };
|
|
301
|
-
// A preview is a branch of its source workspace, and the branch's
|
|
302
|
-
// agents need the same credentials the source's do. Its own store is
|
|
303
|
-
// still consulted first, so a preview can override one on purpose.
|
|
304
|
-
const source = platform.previewSourceOf(tenant, workspace);
|
|
305
|
-
if (source) {
|
|
306
|
-
const inherited = decrypt(tenant, read(tenant, source)[name]);
|
|
307
|
-
if (inherited !== null)
|
|
308
|
-
return { value: inherited, scope: "workspace" };
|
|
309
|
-
}
|
|
310
299
|
}
|
|
311
300
|
const shared = decrypt(tenant, read(tenant)[name]);
|
|
312
301
|
return shared === null ? null : { value: shared, scope: "account" };
|
package/dist/src/starter.js
CHANGED
|
@@ -373,8 +373,8 @@ Debugging a failed run is usually: \`runs\` to find it, \`runs/<id>\` to read wh
|
|
|
373
373
|
step failed and what it cost, fix the markdown, \`deploy\`, then \`rerun\`.
|
|
374
374
|
|
|
375
375
|
Every route, with request and response shapes, is in \`docs/api.md\` in the
|
|
376
|
-
foldrun repository — including secrets, keys, billing, OAuth, webhooks and
|
|
377
|
-
|
|
376
|
+
foldrun repository — including secrets, keys, billing, OAuth, webhooks and
|
|
377
|
+
restore.
|
|
378
378
|
|
|
379
379
|
## Writing a flow
|
|
380
380
|
|
package/dist/src/store.d.ts
CHANGED
|
@@ -754,15 +754,6 @@ export interface WorkspaceSummary {
|
|
|
754
754
|
flows: number;
|
|
755
755
|
deployedAt: string;
|
|
756
756
|
runCount: number;
|
|
757
|
-
/** Set when this workspace is a branch's preview of another (preview.ts):
|
|
758
|
-
* which one, which branch, which commit. Read from the marker beside the
|
|
759
|
-
* directory here rather than through preview.ts, which imports this file. */
|
|
760
|
-
preview?: {
|
|
761
|
-
source: string;
|
|
762
|
-
branch: string;
|
|
763
|
-
commit: string;
|
|
764
|
-
at: string;
|
|
765
|
-
};
|
|
766
757
|
/** The workspace's picture, as a URL, when the page that lists it looked
|
|
767
758
|
* one up (avatars.ts) — the store itself does not. */
|
|
768
759
|
avatar?: string | null;
|
|
@@ -824,7 +815,7 @@ export interface ChangeMeta {
|
|
|
824
815
|
}
|
|
825
816
|
export declare function writeWorkspaceFile(tenant: string, workspace: string, rel: string, content: string, meta?: ChangeMeta): void;
|
|
826
817
|
/** Tell the platform, and never let its listener fail a write. */
|
|
827
|
-
declare function workspaceChanged(tenant: string, workspace: string, why: "write" | "deploy" | "
|
|
818
|
+
declare function workspaceChanged(tenant: string, workspace: string, why: "write" | "deploy" | "delete"): void;
|
|
828
819
|
export { workspaceChanged as notifyWorkspaceChanged };
|
|
829
820
|
export declare function syncBundleFor(file: string, change?: "Creation" | "Update"): void;
|
|
830
821
|
/**
|
|
@@ -1144,6 +1135,12 @@ export interface StepRecord {
|
|
|
1144
1135
|
*/
|
|
1145
1136
|
computeSecs?: number | null;
|
|
1146
1137
|
startupSecs?: number | null;
|
|
1138
|
+
/** When each start-up phase ended, in seconds since the executor began
|
|
1139
|
+
* the step — `services`, `applied`, `ready`, `files`, `go`, `shim`,
|
|
1140
|
+
* `system`, `driver`, `imports`, `tools`, `model` (run-container.ts
|
|
1141
|
+
* #STARTUP_PHASES). Only the phases this step went through; null from
|
|
1142
|
+
* an executor that marks none, absent on steps from before 17 Oct 2026. */
|
|
1143
|
+
startup?: Record<string, number> | null;
|
|
1147
1144
|
/** Which reservation class the sandbox held (small | large), and the
|
|
1148
1145
|
* actual limits it held, as numbers. Facts of the step: a price is per
|
|
1149
1146
|
* core-second, so what was reserved must come from the record, never
|
|
@@ -1288,6 +1285,14 @@ export declare function listRunOutputs(tenant: string, workspace: string, runId:
|
|
|
1288
1285
|
export declare function runOutputPath(tenant: string, workspace: string, runId: string, agent: string, rel: string): string | null;
|
|
1289
1286
|
export declare function readRun(tenant: string, workspace: string, runId: string): RunRecord | null;
|
|
1290
1287
|
export declare function writeRun(tenant: string, workspace: string, run: RunRecord): void;
|
|
1288
|
+
/**
|
|
1289
|
+
* The workspace's live runs — queued, running, or parked at a gate — read
|
|
1290
|
+
* whole, newest first. Off the run index: only the live records are opened,
|
|
1291
|
+
* where listRuns parses every record the workspace ever kept. What the
|
|
1292
|
+
* periodic sweeps want (reconcile, the watchdog), and on a box that keeps
|
|
1293
|
+
* ninety days of runs that is a handful of files instead of thousands.
|
|
1294
|
+
*/
|
|
1295
|
+
export declare function liveRuns(tenant: string, workspace: string): RunRecord[];
|
|
1291
1296
|
export interface RunWithWorkspace extends RunRecord {
|
|
1292
1297
|
workspace: string;
|
|
1293
1298
|
}
|
package/dist/src/store.js
CHANGED
|
@@ -1681,7 +1681,7 @@ export function mayApprove(flow, who) {
|
|
|
1681
1681
|
}
|
|
1682
1682
|
/** A live run of this flow — the fact overlap: decisions are made on. */
|
|
1683
1683
|
export function flowHasLiveRun(tenant, workspace, flowName) {
|
|
1684
|
-
return
|
|
1684
|
+
return listRunSummaries(tenant, workspace).some((r) => r.flow === flowName && (r.status === "running" || r.status === "queued" || r.status === "awaiting-approval"));
|
|
1685
1685
|
}
|
|
1686
1686
|
// Split a flow file into its frontmatter, any prose before the first step,
|
|
1687
1687
|
// and one block per step. Blocks come back in the same order as parseFlow's
|
|
@@ -2012,13 +2012,6 @@ export function listWorkspaces(tenant) {
|
|
|
2012
2012
|
}
|
|
2013
2013
|
}
|
|
2014
2014
|
const runsDir = path.join(pDir, "runs");
|
|
2015
|
-
let preview;
|
|
2016
|
-
try {
|
|
2017
|
-
preview = JSON.parse(fs.readFileSync(path.join(dir, `.${name}.preview.json`), "utf8"));
|
|
2018
|
-
}
|
|
2019
|
-
catch {
|
|
2020
|
-
// not a preview
|
|
2021
|
-
}
|
|
2022
2015
|
return {
|
|
2023
2016
|
name,
|
|
2024
2017
|
description,
|
|
@@ -2026,7 +2019,6 @@ export function listWorkspaces(tenant) {
|
|
|
2026
2019
|
flows: listFlows(tenant, name).length,
|
|
2027
2020
|
deployedAt: fs.statSync(pDir).mtime.toISOString(),
|
|
2028
2021
|
runCount: fs.existsSync(runsDir) ? fs.readdirSync(runsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).length : 0,
|
|
2029
|
-
...(preview ? { preview } : {}),
|
|
2030
2022
|
};
|
|
2031
2023
|
})
|
|
2032
2024
|
.sort((a, b) => b.deployedAt.localeCompare(a.deployedAt));
|
|
@@ -2697,6 +2689,24 @@ export function writeRun(tenant, workspace, run) {
|
|
|
2697
2689
|
fs.renameSync(tmp, p);
|
|
2698
2690
|
noteRunWritten(path.dirname(p), run, p);
|
|
2699
2691
|
}
|
|
2692
|
+
/**
|
|
2693
|
+
* The workspace's live runs — queued, running, or parked at a gate — read
|
|
2694
|
+
* whole, newest first. Off the run index: only the live records are opened,
|
|
2695
|
+
* where listRuns parses every record the workspace ever kept. What the
|
|
2696
|
+
* periodic sweeps want (reconcile, the watchdog), and on a box that keeps
|
|
2697
|
+
* ninety days of runs that is a handful of files instead of thousands.
|
|
2698
|
+
*/
|
|
2699
|
+
export function liveRuns(tenant, workspace) {
|
|
2700
|
+
const out = [];
|
|
2701
|
+
for (const s of listRunSummaries(tenant, workspace)) {
|
|
2702
|
+
if (!LIVE_STATUSES.has(s.status))
|
|
2703
|
+
continue;
|
|
2704
|
+
const run = readRun(tenant, workspace, s.id);
|
|
2705
|
+
if (run)
|
|
2706
|
+
out.push(run);
|
|
2707
|
+
}
|
|
2708
|
+
return out;
|
|
2709
|
+
}
|
|
2700
2710
|
/**
|
|
2701
2711
|
* Every run in the account, newest first, off the per-workspace index.
|
|
2702
2712
|
*
|
|
@@ -2721,6 +2731,13 @@ export function listAllRuns(tenant) {
|
|
|
2721
2731
|
}))
|
|
2722
2732
|
.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
2723
2733
|
}
|
|
2734
|
+
/**
|
|
2735
|
+
* Which version of a file this is. mtime alone missed a rewrite inside one
|
|
2736
|
+
* mtime tick (coarse on some NFS servers), leaving a stale row that the
|
|
2737
|
+
* sweeps act on; every write here is a rename to a new file, so the inode
|
|
2738
|
+
* moves on each write whatever the clock says, and the size usually does.
|
|
2739
|
+
*/
|
|
2740
|
+
const fileStamp = (st) => `${st.mtimeMs}:${st.ino}:${st.size}`;
|
|
2724
2741
|
const RUN_INDEX = ".index.json";
|
|
2725
2742
|
const LIVE_STATUSES = new Set(["queued", "running", "awaiting-approval"]);
|
|
2726
2743
|
/** One run's row. */
|
|
@@ -2780,22 +2797,22 @@ export function runFromSummary(s) {
|
|
|
2780
2797
|
const runIndexCache = new Map();
|
|
2781
2798
|
function readRunIndex(dir) {
|
|
2782
2799
|
const file = path.join(dir, RUN_INDEX);
|
|
2783
|
-
let
|
|
2800
|
+
let stamp;
|
|
2784
2801
|
try {
|
|
2785
|
-
|
|
2802
|
+
stamp = fileStamp(fs.statSync(file));
|
|
2786
2803
|
}
|
|
2787
2804
|
catch {
|
|
2788
2805
|
runIndexCache.delete(dir);
|
|
2789
2806
|
return null;
|
|
2790
2807
|
}
|
|
2791
2808
|
const cached = runIndexCache.get(dir);
|
|
2792
|
-
if (cached && cached.
|
|
2809
|
+
if (cached && cached.stamp === stamp)
|
|
2793
2810
|
return cached.index;
|
|
2794
2811
|
try {
|
|
2795
2812
|
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
2796
2813
|
if (parsed?.v !== 1 || typeof parsed.runs !== "object" || !parsed.runs)
|
|
2797
2814
|
return null;
|
|
2798
|
-
runIndexCache.set(dir, { index: parsed,
|
|
2815
|
+
runIndexCache.set(dir, { index: parsed, stamp });
|
|
2799
2816
|
return parsed;
|
|
2800
2817
|
}
|
|
2801
2818
|
catch {
|
|
@@ -2808,13 +2825,18 @@ function writeRunIndex(dir, index) {
|
|
|
2808
2825
|
fs.writeFileSync(tmp, JSON.stringify(index));
|
|
2809
2826
|
fs.renameSync(tmp, file);
|
|
2810
2827
|
try {
|
|
2811
|
-
runIndexCache.set(dir, { index,
|
|
2828
|
+
runIndexCache.set(dir, { index, stamp: fileStamp(fs.statSync(file)) });
|
|
2812
2829
|
}
|
|
2813
2830
|
catch {
|
|
2814
2831
|
runIndexCache.delete(dir);
|
|
2815
2832
|
}
|
|
2816
2833
|
}
|
|
2817
|
-
const sameRow = (a, b) =>
|
|
2834
|
+
const sameRow = (a, b) => {
|
|
2835
|
+
if (!a)
|
|
2836
|
+
return false;
|
|
2837
|
+
const { mtimeMs: _m, stamp: _s, ...row } = a;
|
|
2838
|
+
return JSON.stringify(row) === JSON.stringify(b);
|
|
2839
|
+
};
|
|
2818
2840
|
/** writeRun just saved `run` at `file`: bring its row up to date. The file
|
|
2819
2841
|
* is rewritten only when the row's content moved — a live step appends an
|
|
2820
2842
|
* event several times a second, and the row does not change for that;
|
|
@@ -2828,7 +2850,8 @@ function noteRunWritten(dir, run, file) {
|
|
|
2828
2850
|
const row = summarizeRun(run);
|
|
2829
2851
|
if (sameRow(index.runs[run.id], row))
|
|
2830
2852
|
return;
|
|
2831
|
-
|
|
2853
|
+
const st = fs.statSync(file);
|
|
2854
|
+
index.runs[run.id] = { ...row, mtimeMs: st.mtimeMs, stamp: fileStamp(st) };
|
|
2832
2855
|
writeRunIndex(dir, index);
|
|
2833
2856
|
}
|
|
2834
2857
|
catch {
|
|
@@ -2866,19 +2889,20 @@ export function listRunSummaries(tenant, workspace) {
|
|
|
2866
2889
|
continue;
|
|
2867
2890
|
const id = f.slice(0, -".json".length);
|
|
2868
2891
|
present.add(id);
|
|
2869
|
-
let
|
|
2892
|
+
let st;
|
|
2870
2893
|
try {
|
|
2871
|
-
|
|
2894
|
+
st = fs.statSync(path.join(dir, f));
|
|
2872
2895
|
}
|
|
2873
2896
|
catch {
|
|
2874
2897
|
continue; // pruned between the listing and the stat
|
|
2875
2898
|
}
|
|
2899
|
+
const stamp = fileStamp(st);
|
|
2876
2900
|
const row = index.runs[id];
|
|
2877
|
-
if (row && row.
|
|
2901
|
+
if (row && row.stamp === stamp)
|
|
2878
2902
|
continue;
|
|
2879
2903
|
try {
|
|
2880
2904
|
const run = JSON.parse(fs.readFileSync(path.join(dir, f), "utf8"));
|
|
2881
|
-
index.runs[id] = { ...summarizeRun(run), mtimeMs };
|
|
2905
|
+
index.runs[id] = { ...summarizeRun(run), mtimeMs: st.mtimeMs, stamp };
|
|
2882
2906
|
changed = true;
|
|
2883
2907
|
}
|
|
2884
2908
|
catch {
|
|
@@ -2900,7 +2924,7 @@ export function listRunSummaries(tenant, workspace) {
|
|
|
2900
2924
|
}
|
|
2901
2925
|
}
|
|
2902
2926
|
return Object.values(index.runs)
|
|
2903
|
-
.map(({ mtimeMs: _m, ...row }) => row)
|
|
2927
|
+
.map(({ mtimeMs: _m, stamp: _s, ...row }) => row)
|
|
2904
2928
|
.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
2905
2929
|
}
|
|
2906
2930
|
/** Every workspace's runs as summaries, tagged with the workspace. */
|
package/index.ts
CHANGED
|
@@ -36,7 +36,6 @@ export * from "./src/tool-names.ts";
|
|
|
36
36
|
export * from "./src/history.ts";
|
|
37
37
|
export * from "./src/zip.ts";
|
|
38
38
|
export * from "./src/package.ts";
|
|
39
|
-
export * from "./src/gitrepo.ts";
|
|
40
39
|
export * from "./src/storage.ts";
|
|
41
40
|
export * from "./src/secrets.ts";
|
|
42
41
|
export * from "./src/oauth-presets.ts";
|
|
@@ -47,5 +46,3 @@ export * from "./src/approvals.ts";
|
|
|
47
46
|
export * from "./src/deploy.ts";
|
|
48
47
|
export * from "./src/agent-import.ts";
|
|
49
48
|
export * from "./src/account.ts";
|
|
50
|
-
export * from "./src/tar.ts";
|
|
51
|
-
export * from "./src/git.ts";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foldrun/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Agents are just folders. The format, the runner, the checks.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"type": "module",
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
"types": "./dist/index.d.ts",
|
|
10
10
|
"default": "./dist/index.js"
|
|
11
11
|
},
|
|
12
|
-
"./git-hook": "./dist/src/git-hook.js",
|
|
13
12
|
"./*": {
|
|
14
13
|
"types": "./dist/src/*.d.ts",
|
|
15
14
|
"default": "./dist/src/*.js"
|
|
@@ -18,7 +17,6 @@
|
|
|
18
17
|
"foldrun": {
|
|
19
18
|
"publicApi": [
|
|
20
19
|
".",
|
|
21
|
-
"./git-hook",
|
|
22
20
|
"./agents-md",
|
|
23
21
|
"./browser-pod",
|
|
24
22
|
"./flow-patterns",
|