faberun 0.26.0 → 0.26.1
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "faberun",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.1",
|
|
4
4
|
"description": "Faberun is a development orchestration system that turns intent into verified software: harness- and model-agnostic, it keeps the intent, coordinates the work, verifies the result and decides what happens next.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/cli/launch.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import { readRunNodes } from "../run/node-store.mjs";
|
|
|
24
24
|
import { spawn } from "node:child_process";
|
|
25
25
|
import { validateContract } from "../contract/index.mjs";
|
|
26
26
|
import { runDirectory } from "../run/paths.mjs";
|
|
27
|
+
import { livePreflightCeilingSec } from "../engine/live-preflight.mjs";
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* The file a detached child is spawned as. It must be the CLI and not this
|
|
@@ -94,7 +95,7 @@ export function detachArgv(argv, options = {}) {
|
|
|
94
95
|
* @param {number} [timeoutMs]
|
|
95
96
|
* @returns {Promise<BootstrapRecord>}
|
|
96
97
|
*/
|
|
97
|
-
export async function waitForBootstrap(runDir, pid, child = null, timeoutMs =
|
|
98
|
+
export async function waitForBootstrap(runDir, pid, child = null, timeoutMs = bootstrapReadyTimeoutMs()) {
|
|
98
99
|
const deadline = Date.now() + timeoutMs;
|
|
99
100
|
const nonce = child?.bootstrapNonce;
|
|
100
101
|
if (!nonce) throw new Error(`detached bootstrap has no start nonce for pid ${pid}`);
|
|
@@ -157,6 +158,18 @@ export async function waitForBootstrap(runDir, pid, child = null, timeoutMs = 30
|
|
|
157
158
|
cleanupBootstrapAttempts(runDir);
|
|
158
159
|
throw new Error(`detached bootstrap did not become ready for pid ${pid}`);
|
|
159
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* The controller answers ready only after the dispatch gate's live preflight,
|
|
163
|
+
* so the launcher waits for that ask on top of its own 30s start-up margin.
|
|
164
|
+
* Measured 2026-09-27: with a fixed 30s wait, a gate asking Opus at `xhigh`
|
|
165
|
+
* made `run --detach` and `plan --detach` report "did not become ready" for
|
|
166
|
+
* controllers that then came up and ran.
|
|
167
|
+
*
|
|
168
|
+
* @returns {number}
|
|
169
|
+
*/
|
|
170
|
+
export function bootstrapReadyTimeoutMs() {
|
|
171
|
+
return 30_000 + livePreflightCeilingSec() * 1000;
|
|
172
|
+
}
|
|
160
173
|
/**
|
|
161
174
|
* @param {string} runDir
|
|
162
175
|
* @param {BootstrapRecord} bootstrap
|
|
@@ -46,7 +46,7 @@ const LIVE_PREFLIGHT_OUTPUT_LIMIT_BYTES = 512 * 1024;
|
|
|
46
46
|
* whole is part of the answer.
|
|
47
47
|
*
|
|
48
48
|
* @param {string} contractPath
|
|
49
|
-
* @param {{static?: boolean, liveTimeoutSec?: number, persisted?: boolean}} [options]
|
|
49
|
+
* @param {{static?: boolean, liveTimeoutSec?: number, minimumTimeoutSec?: number, persisted?: boolean}} [options]
|
|
50
50
|
* @returns {Promise<ProbeResult[]>}
|
|
51
51
|
*/
|
|
52
52
|
export async function preflightContract(contractPath, options = {}) {
|
|
@@ -70,7 +70,7 @@ export async function preflightContract(contractPath, options = {}) {
|
|
|
70
70
|
* once, so a caller cannot accidentally ask a different question.
|
|
71
71
|
*
|
|
72
72
|
* @param {{runtime: RuntimeSnapshot, requiredCapabilitySets?: import("../harnesses/index.mjs").CapabilityRequirements[]}[]} entries
|
|
73
|
-
* @param {{static?: boolean, liveTimeoutSec?: number, cwd?: string}} [options]
|
|
73
|
+
* @param {{static?: boolean, liveTimeoutSec?: number, minimumTimeoutSec?: number, cwd?: string}} [options]
|
|
74
74
|
* @returns {Promise<ProbeResult[]>}
|
|
75
75
|
*/
|
|
76
76
|
export async function preflightRuntimes(entries, options = {}) {
|
|
@@ -80,7 +80,7 @@ export async function preflightRuntimes(entries, options = {}) {
|
|
|
80
80
|
));
|
|
81
81
|
if (options.static === true) return staticChecks;
|
|
82
82
|
|
|
83
|
-
const
|
|
83
|
+
const configuredTimeoutSec = configuredLivePreflightTimeout(options.liveTimeoutSec);
|
|
84
84
|
let liveRepo;
|
|
85
85
|
try {
|
|
86
86
|
liveRepo = createLivePreflightRepo();
|
|
@@ -95,7 +95,7 @@ export async function preflightRuntimes(entries, options = {}) {
|
|
|
95
95
|
}
|
|
96
96
|
try {
|
|
97
97
|
return await Promise.all(staticChecks.map(async (check, index) => {
|
|
98
|
-
const live = await livePreflight(runtimes[index], liveRepo,
|
|
98
|
+
const live = await livePreflight(runtimes[index], liveRepo, configuredTimeoutSec ?? Math.max(options.minimumTimeoutSec ?? 0, defaultLivePreflightTimeout(runtimes[index])));
|
|
99
99
|
const liveDetail = live.status === "done"
|
|
100
100
|
? `live done · usage ${formatUsage(live.usage)} · cost ${formatCost(live.costUsd)}`
|
|
101
101
|
: `live ${live.status} · ${live.error?.code ?? "provider_error"}: ${redactProviderText(live.error?.message ?? "generation failed")} · usage ${formatUsage(live.usage)} · cost ${formatCost(live.costUsd)}`;
|
|
@@ -116,16 +116,54 @@ export async function preflightRuntimes(entries, options = {}) {
|
|
|
116
116
|
rmSync(liveRepo, { recursive: true, force: true });
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
|
-
/**
|
|
120
|
-
|
|
119
|
+
/**
|
|
120
|
+
* An explicit budget (option, then environment) applies to every runtime.
|
|
121
|
+
*
|
|
122
|
+
* @param {number|undefined} configured
|
|
123
|
+
* @returns {number|null}
|
|
124
|
+
*/
|
|
125
|
+
function configuredLivePreflightTimeout(configured) {
|
|
121
126
|
const raw = configured ?? (process.env.FABERUN_PREFLIGHT_TIMEOUT_SEC === undefined
|
|
122
|
-
?
|
|
127
|
+
? null
|
|
123
128
|
: Number(process.env.FABERUN_PREFLIGHT_TIMEOUT_SEC));
|
|
129
|
+
if (raw === null) return null;
|
|
124
130
|
if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0) {
|
|
125
131
|
throw new TypeError("preflight live timeout must be a positive number of seconds");
|
|
126
132
|
}
|
|
127
133
|
return raw;
|
|
128
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Without an explicit budget, the wait follows the runtime's reasoning effort:
|
|
137
|
+
* a model at high effort thinks before answering even a one-word prompt.
|
|
138
|
+
* Measured 2026-09-26: Opus 5.5 at `xhigh` failed every `faberun plan`
|
|
139
|
+
* preflight at 15 s and passed once the budget was raised, so the planning
|
|
140
|
+
* pipeline was refusing a runtime that was healthy.
|
|
141
|
+
*
|
|
142
|
+
* @param {RuntimeSnapshot} runtime
|
|
143
|
+
* @returns {number}
|
|
144
|
+
*/
|
|
145
|
+
export function defaultLivePreflightTimeout(runtime) {
|
|
146
|
+
// Keep LIVE_PREFLIGHT_CEILING_SEC equal to the largest value returned here.
|
|
147
|
+
const reasoning = /** @type {{reasoning?: unknown}} */ (runtime).reasoning;
|
|
148
|
+
if (reasoning === "xhigh" || reasoning === "max") return 180;
|
|
149
|
+
if (reasoning === "high") return 60;
|
|
150
|
+
return 15;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The longest a live preflight can take without an explicit budget: what a
|
|
154
|
+
* detached launcher must be willing to wait before the controller it spawned
|
|
155
|
+
* can report ready, because the dispatch gate asks before it does.
|
|
156
|
+
*/
|
|
157
|
+
export const LIVE_PREFLIGHT_CEILING_SEC = 180;
|
|
158
|
+
/**
|
|
159
|
+
* The longest one live preflight ask may take under the current environment.
|
|
160
|
+
*
|
|
161
|
+
* @returns {number}
|
|
162
|
+
*/
|
|
163
|
+
export function livePreflightCeilingSec() {
|
|
164
|
+
const override = Number(process.env.FABERUN_PREFLIGHT_TIMEOUT_SEC);
|
|
165
|
+
return process.env.FABERUN_PREFLIGHT_TIMEOUT_SEC !== undefined && Number.isFinite(override) && override > 0 ? override : LIVE_PREFLIGHT_CEILING_SEC;
|
|
166
|
+
}
|
|
129
167
|
/** @returns {string} */
|
|
130
168
|
function createLivePreflightRepo() {
|
|
131
169
|
const directory = mkdtempSync(join(tmpdir(), "faberun-preflight-"));
|
|
@@ -507,11 +507,12 @@ export async function assertEnvironmentReady(contract, runDir, sourceIdentity) {
|
|
|
507
507
|
// worker and no judge, and can spend no availability.
|
|
508
508
|
if (blocking === null && launchMayDispatch(runDir)) {
|
|
509
509
|
// measured 2026-09-22: asking four routed runtimes in parallel took about
|
|
510
|
-
// 18s, so
|
|
511
|
-
//
|
|
512
|
-
//
|
|
510
|
+
// 18s, so no runtime is given less than 60s here; a runtime at high
|
|
511
|
+
// reasoning effort gets its longer default (defaultLivePreflightTimeout).
|
|
512
|
+
// FABERUN_PREFLIGHT_TIMEOUT_SEC stays the operator override for every
|
|
513
|
+
// runtime; preflightContract validates it, so only a valid number is lifted.
|
|
513
514
|
const override = Number(process.env.FABERUN_PREFLIGHT_TIMEOUT_SEC);
|
|
514
|
-
const timeoutSec = process.env.FABERUN_PREFLIGHT_TIMEOUT_SEC !== undefined && Number.isFinite(override) && override > 0 ? override :
|
|
515
|
+
const timeoutSec = process.env.FABERUN_PREFLIGHT_TIMEOUT_SEC !== undefined && Number.isFinite(override) && override > 0 ? override : undefined;
|
|
515
516
|
const probes = await livePreflightProbes(contract, runDir, sourceIdentity, timeoutSec);
|
|
516
517
|
const silent = probes.filter((probe) => liveSilenceCause(probe) !== null);
|
|
517
518
|
writeJsonAtomic(join(runDir, "env-preflight.json"), evidence(silent.length === 0, probes));
|
|
@@ -536,6 +537,8 @@ export async function assertEnvironmentReady(contract, runDir, sourceIdentity) {
|
|
|
536
537
|
throw Object.assign(new Error(`env_preflight_failed: ${blocking} · the run stays resumable: fix the environment and resume ${runDir}`), { code: "env_preflight_failed" });
|
|
537
538
|
}
|
|
538
539
|
|
|
540
|
+
/** No routed runtime is given less than this by the dispatch gate. */
|
|
541
|
+
const GATE_MINIMUM_PREFLIGHT_SEC = 60;
|
|
539
542
|
/**
|
|
540
543
|
* The live half of the gate for one launch. Every routed runtime either holds
|
|
541
544
|
* a verdict this machine recorded inside its freshness window -- reused, the
|
|
@@ -552,7 +555,7 @@ export async function assertEnvironmentReady(contract, runDir, sourceIdentity) {
|
|
|
552
555
|
* @param {ValidatedContract} contract
|
|
553
556
|
* @param {string} runDir
|
|
554
557
|
* @param {SourceIdentity|undefined} sourceIdentity
|
|
555
|
-
* @param {number} timeoutSec
|
|
558
|
+
* @param {number|undefined} timeoutSec
|
|
556
559
|
* @returns {Promise<ProbeResult[]>}
|
|
557
560
|
*/
|
|
558
561
|
async function livePreflightProbes(contract, runDir, sourceIdentity, timeoutSec) {
|
|
@@ -628,7 +631,7 @@ async function livePreflightProbes(contract, runDir, sourceIdentity, timeoutSec)
|
|
|
628
631
|
};
|
|
629
632
|
});
|
|
630
633
|
}
|
|
631
|
-
const probes = await preflightContract(join(runDir, "contract.json"), { liveTimeoutSec: timeoutSec, persisted: true });
|
|
634
|
+
const probes = await preflightContract(join(runDir, "contract.json"), { ...(timeoutSec === undefined ? {} : { liveTimeoutSec: timeoutSec }), minimumTimeoutSec: GATE_MINIMUM_PREFLIGHT_SEC, persisted: true });
|
|
632
635
|
// What this launch bought is durable from here on: every ask that reached a
|
|
633
636
|
// provider -- a refusal included, an answer being an answer -- is recorded
|
|
634
637
|
// under the provider's own identity. Silence and a command that never
|