bareloop 0.21.0 → 0.23.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/CHANGELOG.md +292 -0
- package/README.md +1 -1
- package/bareloop.context.md +177 -24
- package/package.json +3 -3
- package/src/authorflow.js +7 -2
- package/src/authorjob.js +15 -1
- package/src/authorscout.js +83 -9
- package/src/bundle.js +40 -46
- package/src/calibrate.js +10 -0
- package/src/cli.js +52 -19
- package/src/close-integrity.js +325 -0
- package/src/closetimeout.js +165 -0
- package/src/declaredclose.js +9 -2
- package/src/index.js +14 -0
- package/src/job.js +84 -5
- package/src/judged.js +108 -5
- package/src/kinds.js +12 -0
- package/src/plan.js +5 -1
- package/src/planrun.js +392 -46
- package/src/providers.js +176 -0
- package/src/ralph.js +40 -7
- package/src/ratelimit.js +116 -0
- package/src/reuse.js +12 -2
- package/src/reviewdoor.js +26 -6
- package/src/run.js +83 -8
- package/src/transport.js +29 -12
- package/src/validate.js +59 -0
- package/types/authorscout.d.ts +27 -2
- package/types/close-integrity.d.ts +196 -0
- package/types/closetimeout.d.ts +126 -0
- package/types/declaredclose.d.ts +6 -1
- package/types/index.d.ts +1 -0
- package/types/job.d.ts +4 -0
- package/types/judged.d.ts +7 -2
- package/types/kinds.d.ts +9 -0
- package/types/planrun.d.ts +17 -2
- package/types/providers.d.ts +104 -0
- package/types/ralph.d.ts +19 -5
- package/types/ratelimit.d.ts +59 -0
- package/types/reuse.d.ts +2 -0
- package/types/reviewdoor.d.ts +3 -1
- package/types/run.d.ts +33 -2
- package/types/transport.d.ts +10 -4
- package/types/validate.d.ts +48 -0
package/src/planrun.js
CHANGED
|
@@ -43,6 +43,10 @@ import {
|
|
|
43
43
|
} from './kinds.js';
|
|
44
44
|
import { workBranchName, prepareWorkBranch } from './workbranch.js';
|
|
45
45
|
import { TRANSPORT_MAX_ATTEMPTS, isTransportFailure } from './transport.js';
|
|
46
|
+
import { isRateLimited, rateLimitWaitMs, statedWaitMs } from './ratelimit.js';
|
|
47
|
+
import { setTimeout as sleepMs } from 'node:timers/promises';
|
|
48
|
+
import { checkCloseAbsolutePaths, checkCloseByteSignature, checkStageByteSignature, checkCloseDirRequired } from './close-integrity.js';
|
|
49
|
+
import { resolveCloseTimeoutMs, closeTimeoutBanner, TIMING_PREFLIGHT_CEILING_MS } from './closetimeout.js';
|
|
46
50
|
|
|
47
51
|
const require = createRequire(import.meta.url);
|
|
48
52
|
const { Loop, Retry, wireGate, HaltError } = require('bare-agent');
|
|
@@ -70,21 +74,44 @@ const { Loop, Retry, wireGate, HaltError } = require('bare-agent');
|
|
|
70
74
|
const transportRetry = new Retry({ maxAttempts: TRANSPORT_MAX_ATTEMPTS, retryOn: isTransportFailure, timeout: 0 });
|
|
71
75
|
|
|
72
76
|
/**
|
|
73
|
-
* Wrap a Loop provider's `generate`
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
77
|
+
* Wrap a Loop provider's `generate` with TWO independent, one-shot retry
|
|
78
|
+
* ladders: F115's transport-class retry (unchanged), and PRD item 28's
|
|
79
|
+
* parked (a/b) ruling — hamr's verbatim call: "do a/b as a fallback, and
|
|
80
|
+
* verify/validate + no regression" — a bounded retry on an HTTP 429 that
|
|
81
|
+
* honours the vendor's OWN stated wait (src/ratelimit.js). Every other
|
|
82
|
+
* property of `baseProvider` (`.name`, `.model`, `.ownsCycle`, `.policy`)
|
|
83
|
+
* passes through unchanged — bare-agent's Loop reads those directly
|
|
84
|
+
* (src/loop.js).
|
|
79
85
|
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
86
|
+
* The two budgets are independent — one transport retry AND one rate-limit
|
|
87
|
+
* retry may both occur across a single `generate()` call's lifetime, but
|
|
88
|
+
* each at most once. This is a FALLBACK, not the primary answer: the
|
|
89
|
+
* primary answer (F139) is picking a worker model with adequate rate
|
|
90
|
+
* headroom; this softens the failure mode when that was not enough, it does
|
|
91
|
+
* not replace it.
|
|
92
|
+
*
|
|
93
|
+
* F64 carve-out (both ladders): a wall-DERIVED timeout (bare-agent's own
|
|
94
|
+
* `ETIMEDOUT`/`EDEADLINE`, thrown because `clock.callTimeoutMs()` told the
|
|
95
|
+
* provider to cut the call — `isWallTimeout`, src/clock.js) is doctrine's
|
|
96
|
+
* governance stop, never a casualty to retry, and retrying it would both
|
|
97
|
+
* mis-spend a round the operator's clock already ended and corrupt
|
|
98
|
+
* `categorize`'s own classification (which reads the SAME error a moment
|
|
99
|
+
* later). The wall always wins over either retry shape: before EITHER retry,
|
|
100
|
+
* `isWallTimeout` must be false, and before sleeping for a rate-limit retry,
|
|
101
|
+
* the run must have MORE time left than the wait would cost
|
|
102
|
+
* (`clock.remainingMs() > waitMs`) — a wait that would run past the
|
|
103
|
+
* deadline is not honoured; the run ends on the operator's clock instead of
|
|
104
|
+
* sleeping past it.
|
|
105
|
+
*
|
|
106
|
+
* `spendComplete` note (the one place the two ladders differ): a transport
|
|
107
|
+
* throw's first attempt may already have been billed (fetch failing after
|
|
108
|
+
* the request left the socket), so F115 floors `spendComplete` for the rest
|
|
109
|
+
* of the run regardless of `recovered` (src/run.js reads the `transport-retry`
|
|
110
|
+
* record's presence for this). A 429, in contrast, is a REFUSAL — the vendor
|
|
111
|
+
* rejected the request before processing it — so nothing can have been
|
|
112
|
+
* billed, and a `rate-limit-retry` record does NOT floor `spendComplete`
|
|
113
|
+
* (src/run.js only watches for `transport-retry`; this is a stated
|
|
114
|
+
* assumption, not independently measured against a real provider).
|
|
88
115
|
*
|
|
89
116
|
* Scope note: this wraps the ONE seam that builds every worker Loop
|
|
90
117
|
* (scout/drafter/step-worker/fix-worker, planrun.js `newLoop` ~line 2240) —
|
|
@@ -101,38 +128,110 @@ const transportRetry = new Retry({ maxAttempts: TRANSPORT_MAX_ATTEMPTS, retryOn:
|
|
|
101
128
|
* @param {(s: string) => string} scrub
|
|
102
129
|
* @param {import('./clock.js').Clock} clock
|
|
103
130
|
*/
|
|
104
|
-
|
|
131
|
+
// F146/F149 (PRD item 30.5) — every provider construction site in this repo now
|
|
132
|
+
// passes `exposeErrorBody: true` (bare-agent's providers), which attaches the
|
|
133
|
+
// parsed upstream error body as `err.body` on an HTTP throw. The body often
|
|
134
|
+
// carries the ONE sentence that names what actually broke — the category and
|
|
135
|
+
// HTTP status alone never do (F146: Qwen's `HTTP 400` said nothing on its own;
|
|
136
|
+
// its body said "System message must be at the beginning"). This was PARKED
|
|
137
|
+
// (2026-09-05 design note) because a raw body can echo the API key straight
|
|
138
|
+
// back — so it goes through `redactSecrets`, the ONE secret shape inventory
|
|
139
|
+
// (`SECRET_PATTERNS`/`scanSecrets`/`redactSecrets`, `src/validate.js`),
|
|
140
|
+
// REGARDLESS of whatever `scrub` a caller has in scope, and is bounded at
|
|
141
|
+
// `BODY_MAX` chars (600 — bounded like every other detail this file emits:
|
|
142
|
+
// `CHECK_GAP_MAX`, `SCOUT_BLOB_MAX`) so one oversized body can never blow a
|
|
143
|
+
// record open. Only this redacted, capped STRING ever rides an emit — the raw
|
|
144
|
+
// `e.body` object itself must never reach a spine record.
|
|
145
|
+
const BODY_MAX = 600;
|
|
146
|
+
const BODY_MARKER = ' — body: ';
|
|
147
|
+
/** @param {any} e @returns {string} '' when there is no body, else ' — body: <redacted, ≤BODY_MAX chars>' */
|
|
148
|
+
const bodySuffix = (e) => {
|
|
149
|
+
if (e?.body === undefined) return '';
|
|
150
|
+
let raw;
|
|
151
|
+
try { raw = JSON.stringify(e.body); } catch { raw = String(e.body); }
|
|
152
|
+
return `${BODY_MARKER}${redactSecrets(raw).slice(0, BODY_MAX)}`;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
function withProviderRetries(baseProvider, phase, emit, scrub, clock) {
|
|
105
156
|
return new Proxy(baseProvider, {
|
|
106
157
|
get(target, prop, receiver) {
|
|
107
158
|
if (prop !== 'generate') return Reflect.get(target, prop, receiver);
|
|
108
159
|
return async (...args) => {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
160
|
+
// Each budget is consumed at most once across the WHOLE call,
|
|
161
|
+
// however many times the two ladders alternate.
|
|
162
|
+
let transportBudget = 1;
|
|
163
|
+
let rateLimitBudget = 1;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* One "transport-protected" attempt: calls `target.generate` through
|
|
167
|
+
* the shared `transportRetry` instance, which itself may retry once
|
|
168
|
+
* for a transport-class throw — but only while `transportBudget`
|
|
169
|
+
* is still available, so a SECOND pass through this function (after
|
|
170
|
+
* a rate-limit sleep) cannot spend a transport retry twice.
|
|
171
|
+
*/
|
|
172
|
+
const attemptWithTransport = async () => {
|
|
173
|
+
let calls = 0;
|
|
174
|
+
/** @type {any} */
|
|
175
|
+
let firstErr = null;
|
|
176
|
+
const fn = async () => {
|
|
177
|
+
calls += 1;
|
|
178
|
+
try {
|
|
179
|
+
return await target.generate(...args);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
if (calls === 1) firstErr = err;
|
|
182
|
+
throw err;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
113
185
|
try {
|
|
114
|
-
|
|
186
|
+
const result = await transportRetry.call(fn, {
|
|
187
|
+
retryOn: (err) => transportBudget > 0 && isTransportFailure(err) && !isWallTimeout(err, clock),
|
|
188
|
+
});
|
|
189
|
+
if (calls > 1) {
|
|
190
|
+
transportBudget -= 1;
|
|
191
|
+
const message = String(firstErr?.message ?? firstErr ?? '').slice(0, 200) + bodySuffix(firstErr);
|
|
192
|
+
emit('transport-retry', { phase, attempt: 1, error: scrub(message), recovered: true });
|
|
193
|
+
}
|
|
194
|
+
return result;
|
|
115
195
|
} catch (err) {
|
|
116
|
-
if (
|
|
196
|
+
if (calls > 1) {
|
|
197
|
+
transportBudget -= 1;
|
|
198
|
+
const message = String(firstErr?.message ?? firstErr ?? '').slice(0, 200) + bodySuffix(firstErr);
|
|
199
|
+
emit('transport-retry', { phase, attempt: 1, error: scrub(message), recovered: false });
|
|
200
|
+
}
|
|
117
201
|
throw err;
|
|
118
202
|
}
|
|
119
203
|
};
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
204
|
+
|
|
205
|
+
/** The whole call, recursing (at most once) through a rate-limit sleep. */
|
|
206
|
+
const attempt = async () => {
|
|
207
|
+
try {
|
|
208
|
+
return await attemptWithTransport();
|
|
209
|
+
} catch (err) {
|
|
210
|
+
if (rateLimitBudget <= 0 || !isRateLimited(err) || isWallTimeout(err, clock)) throw err;
|
|
211
|
+
|
|
212
|
+
const waitMs = rateLimitWaitMs(err);
|
|
213
|
+
const remainingMs = clock.remainingMs();
|
|
214
|
+
// The wall always wins: no retry when nothing parses to an
|
|
215
|
+
// honourable wait, and none when the run has no more time left
|
|
216
|
+
// than the wait itself would cost.
|
|
217
|
+
if (waitMs == null || (remainingMs !== Infinity && remainingMs <= waitMs)) throw err;
|
|
218
|
+
|
|
219
|
+
rateLimitBudget -= 1;
|
|
220
|
+
const stated = statedWaitMs(err);
|
|
221
|
+
const message = String(err?.message ?? err ?? '').slice(0, 200) + bodySuffix(err);
|
|
222
|
+
await sleepMs(waitMs, undefined, { ref: false });
|
|
223
|
+
try {
|
|
224
|
+
const result = await attempt();
|
|
225
|
+
emit('rate-limit-retry', { phase, attempt: 1, statedMs: stated, waitedMs: waitMs, error: scrub(message), recovered: true });
|
|
226
|
+
return result;
|
|
227
|
+
} catch (err2) {
|
|
228
|
+
emit('rate-limit-retry', { phase, attempt: 1, statedMs: stated, waitedMs: waitMs, error: scrub(message), recovered: false });
|
|
229
|
+
throw err2;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
127
232
|
};
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
report(true);
|
|
131
|
-
return result;
|
|
132
|
-
} catch (err) {
|
|
133
|
-
report(false);
|
|
134
|
-
throw err;
|
|
135
|
-
}
|
|
233
|
+
|
|
234
|
+
return attempt();
|
|
136
235
|
};
|
|
137
236
|
},
|
|
138
237
|
});
|
|
@@ -640,7 +739,14 @@ ${scoutBlob || '(no scout notes)'}`;
|
|
|
640
739
|
* ladder (src/ladder.js). Arbiter territory exactly as the count it replaced was —
|
|
641
740
|
* the runner sets it, the plan cannot express it, and no step may tighten or raise
|
|
642
741
|
* it.
|
|
643
|
-
* @param {number} [opts.closeTimeoutMs] close/check wall-clock cap (shell territory)
|
|
742
|
+
* @param {number} [opts.closeTimeoutMs] close/check wall-clock cap (shell territory).
|
|
743
|
+
* OPTIONAL as of PRD item 27/M3: absent, this run autosets it from a $0 seed
|
|
744
|
+
* timing pass (or the spec's own signed `closeTimeoutMs` override) — an
|
|
745
|
+
* explicit value here still wins outright (backward compatibility/test
|
|
746
|
+
* control only, F133: `scripts/run-u.mjs`/`src/cli.js` never pass this).
|
|
747
|
+
* @param {string|null} [opts.closeDir] the close's own books directory (PRD
|
|
748
|
+
* item 27/M3 Part B), forwarded to every close invocation as
|
|
749
|
+
* `BARELOOP_CLOSE_DIR`
|
|
644
750
|
* @param {number} [opts.maxStepRounds] the shell's per-step rounds ceiling (validatePlan's bound)
|
|
645
751
|
* @param {boolean} [opts.scout=true] operator-only switch (docs/product/SCOUT-CONTRAST.md):
|
|
646
752
|
* `false` on a FRESH run (no `resumeSeed`) skips the survey entirely — `scoutBlob`
|
|
@@ -811,12 +917,18 @@ ${scoutBlob || '(no scout notes)'}`;
|
|
|
811
917
|
* precisely the state the person rejected), and the words reach the PLANNER as new
|
|
812
918
|
* authoring. It is never a `humanRuling` — that answers a close's human STAGE, and
|
|
813
919
|
* a green-class job has none. Empty words are refused at the same seam every door is.
|
|
920
|
+
* @param {boolean} [opts.resumable=true] PRD item 27(c)/F130 — does THIS runner
|
|
921
|
+
* support `--resume`? Forwarded verbatim from `runJob`; `run-u.mjs` leaves it at
|
|
922
|
+
* the default (byte-identical to before this param existed), the bundle CLI
|
|
923
|
+
* passes `false` so the three `--resume`-naming readouts below (`MONEY_OPTIONS`,
|
|
924
|
+
* the resume-plan-red option, the fix-loop terminal) say "resume is `run-u`-only
|
|
925
|
+
* in v1" instead of naming a flag that would fail if typed.
|
|
814
926
|
* @returns {Promise<string>} 'green' | 'already-green' | 'escalated' | 'plan-red' |
|
|
815
927
|
* 'check-red' | 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' |
|
|
816
928
|
* 'branch-red' | 'cap-halt' | 'wall-halt' | 'provider-red' | 'interpreter-red' |
|
|
817
929
|
* 'step-stalled' | 'hitl-pause' | 'hitl-decision-red' | `step-red:<id>`
|
|
818
930
|
*/
|
|
819
|
-
export async function runPlan(job, { workdir, provider, nativeProvider, providerFor, judgeProvider = null, emit, remainingUsd, isUnpriced = () => false, spendComplete = () => true, capRuns = 3, strikeLimit = STRIKE_LIMIT, closeTimeoutMs, maxStepRounds = 40, layerRoot = false, readShim = false, scout = true, scoutRounds = SCOUT_ROUNDS, bridge = null, now, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null, heldRuling = null, priorSpentUsd = 0, reviewDoor = null, doorRerun = null }) {
|
|
931
|
+
export async function runPlan(job, { workdir, provider, nativeProvider, providerFor, judgeProvider = null, emit, remainingUsd, isUnpriced = () => false, spendComplete = () => true, capRuns = 3, strikeLimit = STRIKE_LIMIT, closeTimeoutMs, closeDir = null, maxStepRounds = 40, layerRoot = false, readShim = false, scout = true, scoutRounds = SCOUT_ROUNDS, bridge = null, now, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null, heldRuling = null, priorSpentUsd = 0, reviewDoor = null, doorRerun = null, resumable = true }) {
|
|
820
932
|
// MEMORY-CACHE: what the read shim (src/readshim.js) saved THIS run, summed across
|
|
821
933
|
// every mkWorker's own shim instance (scout, drafter, each step's worker, the fix
|
|
822
934
|
// worker) — one accumulator closed over by all of them, because the shim's ledger
|
|
@@ -916,6 +1028,159 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
916
1028
|
return 'close-unsupported';
|
|
917
1029
|
}
|
|
918
1030
|
|
|
1031
|
+
// ── close-absolute-path (PRD item 27(c), F129's general fix): the SAME
|
|
1032
|
+
// detector `src/bundle.js` already applies at export time, now applied for
|
|
1033
|
+
// EVERY job at run start, before the close-first precheck and before any
|
|
1034
|
+
// provider call. A close script whose CONTENT bakes in an absolute path
|
|
1035
|
+
// that exists on disk judges the wrong tree by construction (F129) — the
|
|
1036
|
+
// spec's own `cmd` field naming the script is the address and stays legal;
|
|
1037
|
+
// only the script's bytes are judged. `readCloseScripts`/
|
|
1038
|
+
// `checkCloseAbsolutePaths` live in `src/close-integrity.js` so this and
|
|
1039
|
+
// `exportBundle` never drift into two spellings of the same rule.
|
|
1040
|
+
{
|
|
1041
|
+
const integrity = checkCloseAbsolutePaths(job, workdir);
|
|
1042
|
+
if (!integrity.ok) {
|
|
1043
|
+
const detail = integrity.reds.map((r) => `${r.stage}: "${r.literal}"`).join('; ');
|
|
1044
|
+
emit('escalation', {
|
|
1045
|
+
category: 'close-absolute-path', decisionReady: true,
|
|
1046
|
+
decision: 'A close script bakes in an absolute path that exists on disk — it would judge the wrong tree by construction (F129), so nothing was run and nothing was spent.',
|
|
1047
|
+
options: ['fix the close script to read the cwd it is given, never a baked-in path', 'abandon the task'],
|
|
1048
|
+
detail,
|
|
1049
|
+
});
|
|
1050
|
+
return 'close-absolute-path';
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// ── close-tampered (PRD item 27/M2, N4's own general fix): the job's
|
|
1055
|
+
// signature covers a close STAGE's byte-exact CONTENT (`close[].sha256`),
|
|
1056
|
+
// not only the `cmd` path naming it — `jobSpecHash` already covers this
|
|
1057
|
+
// field with no hash-function change (it sits inside the signed spec), so
|
|
1058
|
+
// the only thing left to verify is that the SCRIPT ON DISK still is what
|
|
1059
|
+
// was signed. Same seam as `close-absolute-path` immediately above (run
|
|
1060
|
+
// start, $0, before the close-first precheck and before any provider
|
|
1061
|
+
// call) and the same detector module — `checkCloseByteSignature` reuses
|
|
1062
|
+
// `readCloseScripts`, so this and the runtime re-verify inside
|
|
1063
|
+
// `runCloseStages` below can never drift into two spellings of "which
|
|
1064
|
+
// bytes were signed".
|
|
1065
|
+
{
|
|
1066
|
+
const sig = checkCloseByteSignature(job, workdir);
|
|
1067
|
+
if (!sig.ok) {
|
|
1068
|
+
const detail = sig.reds.map((r) => `${r.stage}: expected ${r.expected.slice(0, 12)}… got ${r.actual ? `${r.actual.slice(0, 12)}…` : '<unreadable>'}`).join('; ');
|
|
1069
|
+
emit('escalation', {
|
|
1070
|
+
category: 'close-tampered', decisionReady: true,
|
|
1071
|
+
decision: "A close script's bytes no longer match its signed sha256 — it would run something other than what was approved, so nothing was run and nothing was spent.",
|
|
1072
|
+
options: ['restore the close script to its signed bytes', 're-sign the close if the change is intentional (a new spec hash needs re-approval)', 'abandon the task'],
|
|
1073
|
+
detail,
|
|
1074
|
+
});
|
|
1075
|
+
return 'close-tampered';
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// ── close-dir-required (PRD item 27/M3 Part B): a close script that reads
|
|
1080
|
+
// its own books directory (`BARELOOP_CLOSE_DIR`) and gets none would run
|
|
1081
|
+
// silently against `undefined` — refuse at the same $0 seam as the two
|
|
1082
|
+
// checks immediately above, before any tokens spend.
|
|
1083
|
+
{
|
|
1084
|
+
const dirCheck = checkCloseDirRequired(job, workdir, closeDir);
|
|
1085
|
+
if (!dirCheck.ok) {
|
|
1086
|
+
const detail = dirCheck.reds.map((r) => `${r.stage}: ${r.path}`).join('; ');
|
|
1087
|
+
emit('escalation', {
|
|
1088
|
+
category: 'close-dir-required', decisionReady: true,
|
|
1089
|
+
decision: 'A close script reads BARELOOP_CLOSE_DIR (its own books directory, deliberately outside the patient tree) but the runner was given none — nothing was run and nothing was spent.',
|
|
1090
|
+
options: ['run this job through a runner that sets closeDir (run-u.mjs, or the bundle CLI)', 'abandon the task'],
|
|
1091
|
+
detail,
|
|
1092
|
+
});
|
|
1093
|
+
return 'close-dir-required';
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// ── close timeout: autoset + signed override (PRD item 27/M3). hamr,
|
|
1098
|
+
// 2026-09-06: "both … autoset and can be override, same like api pricing".
|
|
1099
|
+
// Constants (`CLOSE_TIMEOUT_FLOOR_MS`, `CLOSE_TIMEOUT_K`) are hamr's own
|
|
1100
|
+
// arbiter constants (2026-09-07) — never agent- or adopter-settable.
|
|
1101
|
+
//
|
|
1102
|
+
// Precedence, all $0, before any provider call:
|
|
1103
|
+
// 1. an EXPLICIT `closeTimeoutMs` runtime option (the pre-M3 shell/test
|
|
1104
|
+
// knob) wins outright — kept for backward compatibility and for tests
|
|
1105
|
+
// that force a specific close-timeout scenario; it is never the signed
|
|
1106
|
+
// spec field and prints its own honest word rather than "signed
|
|
1107
|
+
// override". F133 (run mtqwmb9l): this used to also be how
|
|
1108
|
+
// `scripts/run-u.mjs` fed its OWN pre-resolved ceiling through, which
|
|
1109
|
+
// printed the number correctly here but under the wrong word
|
|
1110
|
+
// ("explicit runner override") beside run-u's own already-correct
|
|
1111
|
+
// "estimated"/"signed override" line — two banners for one number,
|
|
1112
|
+
// the second one lying about its source. Fixed by making this the
|
|
1113
|
+
// ONLY caller of `closeTimeoutMs` an operator/runner is meant to use:
|
|
1114
|
+
// `run-u.mjs` no longer resolves-and-passes it at all (see its own
|
|
1115
|
+
// comment); this tier is test/back-compat-only again.
|
|
1116
|
+
// 2. the SIGNED spec field `job.closeTimeoutMs` — the operator's own
|
|
1117
|
+
// number, which may legally sit above OR below the autoset estimate
|
|
1118
|
+
// (F113's rates-passthrough shape).
|
|
1119
|
+
// 3. AUTOSET — a $0 timing pass runs every stage once (ignoring verdicts;
|
|
1120
|
+
// first-red-wins stays for the real precheck below), under the
|
|
1121
|
+
// generous provisional ceiling `TIMING_PREFLIGHT_CEILING_MS` so a
|
|
1122
|
+
// genuinely hung stage still ends. `max(FLOOR, K × slowest)` becomes
|
|
1123
|
+
// the ceiling every close run in this call uses, and a stage that
|
|
1124
|
+
// timed out DURING THE TIMING PASS is `close-timing-red` — the close
|
|
1125
|
+
// cannot run on this machine, and the provider was never called.
|
|
1126
|
+
//
|
|
1127
|
+
// The pass is SKIPPED whenever an override already exists (case 1 or 2):
|
|
1128
|
+
// running every stage a second time just to throw the reading away would
|
|
1129
|
+
// waste real machine time before any provider call, and — measured risk —
|
|
1130
|
+
// a close stage a test or an operator deliberately made slow/hanging would
|
|
1131
|
+
// then block for up to 10 minutes for no purpose. "Guesstimate only when no
|
|
1132
|
+
// rate is given" is the same doctrine F113 already applies to pricing.
|
|
1133
|
+
{
|
|
1134
|
+
const explicitOverride = typeof closeTimeoutMs === 'number' && Number.isFinite(closeTimeoutMs) ? closeTimeoutMs : null;
|
|
1135
|
+
if (explicitOverride !== null) {
|
|
1136
|
+
// A caller-passed runtime option wins outright and skips the timing
|
|
1137
|
+
// pass entirely — this is the pre-M3 shell/test knob (backward
|
|
1138
|
+
// compatibility only, F133: no production runner passes this anymore —
|
|
1139
|
+
// `scripts/run-u.mjs` and `src/cli.js` both leave this call site
|
|
1140
|
+
// nothing to autoset from but the autoset/signed-override tiers below).
|
|
1141
|
+
closeTimeoutMs = explicitOverride;
|
|
1142
|
+
emit('close-timing', { perStage: [], slowestMs: null, slowestName: null, ceilingMs: closeTimeoutMs, source: 'explicit' });
|
|
1143
|
+
console.log(closeTimeoutBanner({ ceilingMs: closeTimeoutMs, source: 'explicit' }));
|
|
1144
|
+
} else {
|
|
1145
|
+
const resolved = await resolveCloseTimeoutMs({ job, stages: stagedClose, cwd: workdir, redact: scrub });
|
|
1146
|
+
if (resolved.timedOut) {
|
|
1147
|
+
// `timedOut` is only ever true alongside a real `timing` reading
|
|
1148
|
+
// (`resolveCloseTimeoutMs` never sets one without the other) — the
|
|
1149
|
+
// cast states that correlation for the checker, which cannot infer
|
|
1150
|
+
// it across the two independent optional fields on its own.
|
|
1151
|
+
const timing = /** @type {NonNullable<typeof resolved.timing>} */ (resolved.timing);
|
|
1152
|
+
const detail = timing.perStage.filter((s) => s.timedOut).map((s) => s.name).join(', ');
|
|
1153
|
+
emit('close-timing', { perStage: timing.perStage, slowestMs: timing.slowestMs, slowestName: timing.slowestName, ceilingMs: null, source: 'estimated' });
|
|
1154
|
+
emit('escalation', {
|
|
1155
|
+
category: 'close-timing-red', decisionReady: true,
|
|
1156
|
+
decision: `A close stage did not finish within the timing preflight's own ceiling (${TIMING_PREFLIGHT_CEILING_MS}ms) — the close cannot run on this machine in a boundable time, so nothing was run and nothing was spent.`,
|
|
1157
|
+
options: ['investigate why the stage hangs (infra/network/resource issue)', 'sign an explicit closeTimeoutMs override once the real duration is known', 'abandon the task'],
|
|
1158
|
+
detail,
|
|
1159
|
+
});
|
|
1160
|
+
return 'close-timing-red';
|
|
1161
|
+
}
|
|
1162
|
+
closeTimeoutMs = /** @type {number} */ (resolved.closeTimeoutMs);
|
|
1163
|
+
emit('close-timing', { ...(resolved.timing ?? { perStage: [], slowestMs: null, slowestName: null, anyTimedOut: false }), ceilingMs: closeTimeoutMs, source: resolved.source });
|
|
1164
|
+
console.log(closeTimeoutBanner({ ceilingMs: closeTimeoutMs, source: resolved.source, slowestMs: resolved.timing?.slowestMs, slowestName: resolved.timing?.slowestName }));
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// ── the wall must be able to fund at least one close under the EFFECTIVE
|
|
1169
|
+
// ceiling just computed (PRD item 27/M3): `job.maxWallMs`'s own static
|
|
1170
|
+
// floor (`MIN_WALL_MS`, src/job.js) assumed the library's un-autoset
|
|
1171
|
+
// default — with autoset, the real per-stage ceiling is only known here, at
|
|
1172
|
+
// run start, so the honest shape is a run-start check, never a silent
|
|
1173
|
+
// clamp (a budget under one close cannot fund its own close, verbatim the
|
|
1174
|
+
// reasoning `maxWallMs`'s own validator red already states).
|
|
1175
|
+
if (typeof job.maxWallMs === 'number' && job.maxWallMs < closeTimeoutMs) {
|
|
1176
|
+
emit('escalation', {
|
|
1177
|
+
category: 'wall-under-close-timeout', decisionReady: true,
|
|
1178
|
+
decision: `maxWallMs (${job.maxWallMs}ms) is under the effective close timeout (${closeTimeoutMs}ms) — a budget under one close cannot fund its own close, so nothing was run and nothing was spent.`,
|
|
1179
|
+
options: ['raise maxWallMs to at least the printed close timeout and rerun (a spec edit, so the new hash needs re-approval)', 'sign a lower closeTimeoutMs override if the measured duration allows it (same re-approval)', 'abandon the task'],
|
|
1180
|
+
});
|
|
1181
|
+
return 'wall-under-close-timeout';
|
|
1182
|
+
}
|
|
1183
|
+
|
|
919
1184
|
// ── THE SIGNER'S ANSWER (N4 §1.4), read before anything costs anything.
|
|
920
1185
|
//
|
|
921
1186
|
// Three gates, and each refuses rather than guesses. A decision that is not one
|
|
@@ -1264,6 +1529,17 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1264
1529
|
* transport-debugging option list for a run that simply ran out of time. A
|
|
1265
1530
|
* thrower that named its own category keeps it (the standing typed-attribution
|
|
1266
1531
|
* rule); only an UNNAMED throw is classified here.
|
|
1532
|
+
*
|
|
1533
|
+
* F146/F149 — this is also the ONE place a categorized error's `.message` gets
|
|
1534
|
+
* the redacted upstream body appended (`bodySuffix`, above). Every downstream
|
|
1535
|
+
* reader of a categorized error's message — `relay()`'s `detail` here, AND
|
|
1536
|
+
* ralph.js's own mid-step dumb-passthrough catch (`detail: String(e.message ||
|
|
1537
|
+
* e)`) reading the SAME error `mkWorker`'s `ask`/`askFrom` threw after routing
|
|
1538
|
+
* it through this function — gets the sentence for free, without ralph.js
|
|
1539
|
+
* importing anything (it stays stdlib-only, PRD §4: fixed, deliberately dumb).
|
|
1540
|
+
* `relay()` re-categorizes an error its own `ask`/`askFrom` already ran through
|
|
1541
|
+
* here, so this must be IDEMPOTENT: `BODY_MARKER` guards a second pass over the
|
|
1542
|
+
* same error from doubling the suffix.
|
|
1267
1543
|
* @param {any} e
|
|
1268
1544
|
* @returns {{ err: CategorizedError, category: string }} the same throw, stamped,
|
|
1269
1545
|
* plus the category as a plain string (so a caller reading it never has to
|
|
@@ -1274,6 +1550,10 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1274
1550
|
const category = e instanceof HaltError ? 'cap-halt'
|
|
1275
1551
|
: (err.category ?? (isWallTimeout(err, clock) ? 'wall-halt' : 'provider-red'));
|
|
1276
1552
|
err.category = category;
|
|
1553
|
+
if (typeof err.message === 'string' && !err.message.includes(BODY_MARKER)) {
|
|
1554
|
+
const suffix = bodySuffix(err);
|
|
1555
|
+
if (suffix) err.message += suffix;
|
|
1556
|
+
}
|
|
1277
1557
|
return { err, category };
|
|
1278
1558
|
};
|
|
1279
1559
|
|
|
@@ -1300,6 +1580,13 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1300
1580
|
'revise the goal/spec so the work fits the time (same re-approval)',
|
|
1301
1581
|
'abandon the task',
|
|
1302
1582
|
];
|
|
1583
|
+
/** PRD item 27(c)/F130 — the honest escalation tail. `run-u.mjs` supports
|
|
1584
|
+
* `--resume`; the exported bundle CLI does not (v1) and would print a flag
|
|
1585
|
+
* that fails if typed. `resumable` (default `true`, `run-u`'s existing
|
|
1586
|
+
* lines unchanged) picks which sentence names the resume lever. */
|
|
1587
|
+
const resumeToCapNote = resumable
|
|
1588
|
+
? 'top up budgetUsd and rerun with --resume (resume-to-cap; a spec edit, so the new hash needs re-approval)'
|
|
1589
|
+
: 'resume is `run-u`-only in v1 — top up budgetUsd and re-fire the bundle from the start (a spec edit, so the new hash needs re-approval)';
|
|
1303
1590
|
/** MONEY's levers (PRD v1.46 §2), the same three shapes the wall's are, because a
|
|
1304
1591
|
* money cut is the same KIND of stop: the run is out of an operator-owned
|
|
1305
1592
|
* allowance, not out of capability, and the last verdict rendered stands. hamr's
|
|
@@ -1310,12 +1597,12 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1310
1597
|
* hash, so raising it is a new spec version somebody signs — the library only ever
|
|
1311
1598
|
* names the lever, and never adjusts a budget itself (the permanent hard line). */
|
|
1312
1599
|
const MONEY_OPTIONS = [
|
|
1313
|
-
|
|
1600
|
+
resumeToCapNote,
|
|
1314
1601
|
'revise the goal/spec so the work fits the budget (same re-approval)',
|
|
1315
1602
|
'abandon the task',
|
|
1316
1603
|
];
|
|
1317
1604
|
|
|
1318
|
-
const closeOpts = { timeoutMs: closeTimeoutMs, cwd: workdir };
|
|
1605
|
+
const closeOpts = { timeoutMs: closeTimeoutMs, cwd: workdir, closeDir };
|
|
1319
1606
|
/** D8 — the DECLARED close's seed, filled in below before anything runs.
|
|
1320
1607
|
* `null` for a command close, which has no baseline to measure against. */
|
|
1321
1608
|
/** @type {{seedRef: string}|null} */
|
|
@@ -1329,7 +1616,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1329
1616
|
* The command path is byte-identical to what it was: same call, same options.
|
|
1330
1617
|
* @param {any[]} stages
|
|
1331
1618
|
*/
|
|
1332
|
-
const
|
|
1619
|
+
const runCloseStagesInner = (stages) => (declared
|
|
1333
1620
|
? runDeclaredStages(stages, scrub, {
|
|
1334
1621
|
...closeOpts,
|
|
1335
1622
|
seedRef: /** @type {any} */ (declaredCtx).seedRef,
|
|
@@ -1346,6 +1633,13 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1346
1633
|
// an attempt's (a budget funds the attempt PLUS its close), and a null cost trips
|
|
1347
1634
|
// the same F6 pricing halt rather than being laundered into $0.
|
|
1348
1635
|
judgeLoop: judgeProvider ? (/** @type {{system: string}} */ o) => defaultJudgeLoop({ provider: judgeProvider, system: o.system }) : null,
|
|
1636
|
+
// F152/PRD 30.4 — the judge's locate call carried NO time bound at all: a
|
|
1637
|
+
// live endpoint that accepts and never answers hung the process forever
|
|
1638
|
+
// (measured), because an open socket is an active handle and no backstop
|
|
1639
|
+
// can fire on it. The SAME derivation every worker round already uses,
|
|
1640
|
+
// handed through as the FUNCTION (not a snapshot) so a retried locate call
|
|
1641
|
+
// re-reads the remaining time rather than reusing the first attempt's.
|
|
1642
|
+
callBounds,
|
|
1349
1643
|
onJudgeCost: (/** @type {any} */ c) => emit('judge-round', {
|
|
1350
1644
|
stage: c.stage, path: c.path, attempt: c.attempt, label: c.label, model: c.model,
|
|
1351
1645
|
costUsd: c.costUsd, unpricedRounds: c.unpricedRounds,
|
|
@@ -1359,6 +1653,34 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1359
1653
|
}),
|
|
1360
1654
|
})
|
|
1361
1655
|
: runStages(stages, scrub, closeOpts));
|
|
1656
|
+
/**
|
|
1657
|
+
* PRD item 27/M2 — the re-verify that runs before EVERY close run, precheck
|
|
1658
|
+
* included: `runCloseStagesInner` is the ONE seam every close execution in
|
|
1659
|
+
* this runner goes through (the precheck's `judgeClose`, the preflight
|
|
1660
|
+
* loop, the check-passes seam, and the close-fix loop via its own `judge`
|
|
1661
|
+
* seam), so wrapping it here covers all of them without threading a
|
|
1662
|
+
* verify call through each call site separately — the same reasoning
|
|
1663
|
+
* `checkCloseAbsolutePaths`'s run-start placement already relies on (W4:
|
|
1664
|
+
* "the staging itself lives in ONE place").
|
|
1665
|
+
*
|
|
1666
|
+
* A mismatch never reaches the executor: it returns a `close-tampered`
|
|
1667
|
+
* verdict in the SAME shape `runClose`/`runStages` render (`{verdict,
|
|
1668
|
+
* stage, detail}`), so every downstream reader that already keys off
|
|
1669
|
+
* `Object.hasOwn(CLOSE_FAULTS, v.verdict)` (the precheck, the preflight
|
|
1670
|
+
* loop, the post-fix read, and ralph's own judge-fault routing) treats it
|
|
1671
|
+
* exactly like `crashed`/`timed-out`/`killed` — a forbidden-zone outcome,
|
|
1672
|
+
* never retried, never fed back as a gap.
|
|
1673
|
+
* @param {any[]} stages
|
|
1674
|
+
*/
|
|
1675
|
+
const runCloseStages = async (stages) => {
|
|
1676
|
+
const sig = checkStageByteSignature(stages, workdir);
|
|
1677
|
+
if (!sig.ok) {
|
|
1678
|
+
const r = sig.reds[0];
|
|
1679
|
+
const detail = sig.reds.map((rr) => `${rr.stage}: expected ${rr.expected.slice(0, 12)}… got ${rr.actual ? `${rr.actual.slice(0, 12)}…` : '<unreadable>'}`).join('; ');
|
|
1680
|
+
return { verdict: 'close-tampered', stage: r?.stage, detail };
|
|
1681
|
+
}
|
|
1682
|
+
return runCloseStagesInner(stages);
|
|
1683
|
+
};
|
|
1362
1684
|
/** @type {string|undefined} the stage that rendered the LAST close verdict (Layer R's red-set source) */
|
|
1363
1685
|
let closeStage;
|
|
1364
1686
|
/** @type {any} the LAST close verdict itself (W-2). A wall stop KEEPS the grade
|
|
@@ -2253,7 +2575,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2253
2575
|
// re-spends a subscription turn, not a metered fetch) and is out of scope.
|
|
2254
2576
|
const loopProvider = native
|
|
2255
2577
|
? /** @type {any} */ (nativeProvider)({ policy, maxTurns, hasTools: false })
|
|
2256
|
-
:
|
|
2578
|
+
: withProviderRetries(workerProvider ?? provider, phase, emit, scrub, clock);
|
|
2257
2579
|
// F66 — the stall watchdog. bare-agent's `timeoutMs` bounds socket INACTIVITY,
|
|
2258
2580
|
// not call duration (provider-http.js `req.setTimeout` resets on every byte),
|
|
2259
2581
|
// so a connection that stays alive while producing nothing is invisible to it:
|
|
@@ -2295,7 +2617,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2295
2617
|
const newLoop = (gen) => {
|
|
2296
2618
|
/** @type {any} */
|
|
2297
2619
|
let self = null;
|
|
2298
|
-
/** @param {{costUsd?: number|null, pricing?: string|null, rateSource?: string|null, usage?: any, kind?: string}} arg */
|
|
2620
|
+
/** @param {{costUsd?: number|null, pricing?: string|null, rateSource?: string|null, usage?: any, kind?: string, stopReason?: string|null}} arg */
|
|
2299
2621
|
const metered = async (arg) => {
|
|
2300
2622
|
// The meter fires for EVERY generation, live or abandoned. An orphaned round
|
|
2301
2623
|
// was really billed, and spend that no instrument sees is the one thing this
|
|
@@ -2316,6 +2638,14 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2316
2638
|
...rateSourceFields(arg),
|
|
2317
2639
|
tokens: (arg?.usage?.inputTokens ?? 0) + (arg?.usage?.outputTokens ?? 0),
|
|
2318
2640
|
usage: usageOf(arg?.usage),
|
|
2641
|
+
// PRD item 28 — BA-13: bare-agent's Loop surfaces the round's own neutral
|
|
2642
|
+
// stop reason ('end_turn'|'max_tokens'|'tool_use'|'refusal'|…) on EVERY
|
|
2643
|
+
// onLlmResult call, forwarded verbatim. REPORT-ONLY (nothing here acts on
|
|
2644
|
+
// it — the Loop itself already refuses to execute a 'max_tokens' round's
|
|
2645
|
+
// tool calls, BA-6); null when bare-agent didn't say (pre-BA-13, or a
|
|
2646
|
+
// non-string value), never invented — the same honesty rule every other
|
|
2647
|
+
// provenance field on this record already follows.
|
|
2648
|
+
stopReason: typeof arg?.stopReason === 'string' ? arg.stopReason : null,
|
|
2319
2649
|
});
|
|
2320
2650
|
// Everything BELOW here belongs to the live call only. A superseded call must
|
|
2321
2651
|
// not beat the watch (that is the whole watchdog), must not spend the step's
|
|
@@ -2368,7 +2698,17 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2368
2698
|
*/
|
|
2369
2699
|
const askFrom = async (msgs, prompt) => {
|
|
2370
2700
|
try {
|
|
2371
|
-
|
|
2701
|
+
// `msgs` is a prior `loop.run()`'s returned transcript, which already has
|
|
2702
|
+
// the Loop's `system` PREPENDED (bare-agent Loop.run, loop.js ~482-486).
|
|
2703
|
+
// The new Loop built below is constructed with the same `system` and would
|
|
2704
|
+
// prepend it again, leaving two identical system messages at index 0/1.
|
|
2705
|
+
// Real OpenAI tolerates that; vLLM-class OpenAI-compatible backends 400 on
|
|
2706
|
+
// it ("System message must be at the beginning") — captured evidence:
|
|
2707
|
+
// bareloop-patients/spines-poc-openai/qwen-failing-request-x760ei99.json.
|
|
2708
|
+
// bare-agent's own Loop.chat() (loop.js ~1321) strips a leading system
|
|
2709
|
+
// message before re-running for exactly this reason; do the same here.
|
|
2710
|
+
const rest = msgs[0]?.role === 'system' ? msgs.slice(1) : msgs;
|
|
2711
|
+
return await stallWatch.watch((gen) => newLoop(gen).run([...rest, { role: 'user', content: prompt }], [], { cacheMessages: true, maxTokens: 32000, ...callBounds() }));
|
|
2372
2712
|
} catch (e) {
|
|
2373
2713
|
throw categorize(e).err;
|
|
2374
2714
|
}
|
|
@@ -2415,6 +2755,10 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2415
2755
|
* worker does, so this path needs the same split or it is the blinder route),
|
|
2416
2756
|
* and transport. */
|
|
2417
2757
|
const relay = (/** @type {any} */ e, /** @type {string} */ phase) => {
|
|
2758
|
+
// categorize(e) mutates e.message in place (guarded, idempotent) with the
|
|
2759
|
+
// redacted body suffix — never re-append it here, or a caller whose thrown
|
|
2760
|
+
// error already went through categorize() once (mkWorker's ask/askFrom)
|
|
2761
|
+
// would double it.
|
|
2418
2762
|
const { category } = categorize(e);
|
|
2419
2763
|
const detail = String(e?.message ?? e);
|
|
2420
2764
|
if (category === 'cap-halt') {
|
|
@@ -3462,7 +3806,9 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
3462
3806
|
: `${rep.strikes}/${rep.limit} strikes — the fix loop stopped making progress against the close's own numbers (${t.reading}). Continue, change approach, or stop?`,
|
|
3463
3807
|
options: [
|
|
3464
3808
|
'revise the goal/spec so the work is reachable (a spec edit, so the new hash needs re-approval)',
|
|
3465
|
-
|
|
3809
|
+
resumable
|
|
3810
|
+
? 'top up budgetUsd and rerun with --resume, if the trend above says it was still converging'
|
|
3811
|
+
: 'resume is `run-u`-only in v1 — top up budgetUsd and re-fire the bundle from the start, if the trend above says it was still converging',
|
|
3466
3812
|
'abandon the task',
|
|
3467
3813
|
],
|
|
3468
3814
|
};
|
|
@@ -3576,7 +3922,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
3576
3922
|
// is not passed — it survives only inside the governor, as the bound for the
|
|
3577
3923
|
// case the trend instrument is blind.
|
|
3578
3924
|
middle, judge: async () => judgeClose(), ladder: fixGovernor, emit: emitL, redact: scrub,
|
|
3579
|
-
closeTimeoutMs, cwd: workdir, workerWrites: w.workerWrites,
|
|
3925
|
+
closeTimeoutMs, cwd: workdir, workerWrites: w.workerWrites, closeDir,
|
|
3580
3926
|
});
|
|
3581
3927
|
} catch (e) {
|
|
3582
3928
|
planExecuted();
|