bareloop 0.12.0 → 0.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/CHANGELOG.md +93 -0
- package/bareloop.context.md +106 -2
- package/package.json +1 -1
- package/src/index.js +16 -1
- package/src/ledger.js +102 -0
- package/src/plan.js +43 -2
- package/src/planrun.js +167 -14
- package/src/readshim.js +538 -0
- package/src/reuse.js +3 -1
- package/src/run.js +24 -2
- package/src/tools.js +34 -2
- package/types/index.d.ts +2 -1
- package/types/ledger.d.ts +58 -0
- package/types/plan.d.ts +19 -2
- package/types/planrun.d.ts +37 -2
- package/types/readshim.d.ts +117 -0
- package/types/reuse.d.ts +2 -0
- package/types/run.d.ts +15 -1
- package/types/tools.d.ts +17 -1
package/src/readshim.js
ADDED
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
// THE READ SHIM (L1 pointer + L4 cap) — a per-worker delivery ledger over the
|
|
2
|
+
// read tool. ARM-gated and OFF by default; nothing here runs unless `readShim`
|
|
3
|
+
// names an arm on the run (src/planrun.js wires it, src/run.js plumbs it). WHICH
|
|
4
|
+
// levers an arm carries is `readShimArm` below, and the reasoning for each arm's
|
|
5
|
+
// membership sits with it.
|
|
6
|
+
//
|
|
7
|
+
// The measured shape ($0 replay, 1,844 real `shell_read` actions from 143
|
|
8
|
+
// archived gate audits, cap 24,576 B):
|
|
9
|
+
// - 48% of all reads re-read a file that had not changed since the worker
|
|
10
|
+
// last saw it — those bytes are already in the transcript, re-sent and
|
|
11
|
+
// re-billed every round after.
|
|
12
|
+
// - cap + pointer, keyed on WHAT WAS DELIVERED: 51,853,154 → 17,834,409 bytes
|
|
13
|
+
// (65.6% saved) with ZERO untruthful responses.
|
|
14
|
+
// - cap + pointer, keyed on PATH+HASH (the naive design): 8.2 points cheaper
|
|
15
|
+
// and **250 untruthful responses** — median 73,348 bytes hidden each, 234 of
|
|
16
|
+
// them over 10 KB, worst case a worker holding 24,576 of 105,426 bytes told
|
|
17
|
+
// "unchanged, you already have it". That is BA-17 read-blinding, and the
|
|
18
|
+
// extra savings ARE the lie.
|
|
19
|
+
//
|
|
20
|
+
// So the ledger keys on DELIVERY, never on the path's content hash alone: a
|
|
21
|
+
// pointer is legal only once every byte of the CURRENT content has been handed
|
|
22
|
+
// over. A partly-seen file gets its next unseen slice instead — the response the
|
|
23
|
+
// naive design converts into a lie (194 of them across the corpus).
|
|
24
|
+
//
|
|
25
|
+
// What the shim can and cannot claim, stated plainly: it is truthful about what
|
|
26
|
+
// THIS SEAM delivered. The underlying tool applies its own `maxBytes` bound and
|
|
27
|
+
// says so in its own notice; the shim treats that returned text as the content,
|
|
28
|
+
// because it is exactly what the worker gets. It never claims the worker holds
|
|
29
|
+
// bytes the tool itself withheld.
|
|
30
|
+
//
|
|
31
|
+
// L2 (ship a diff when the content changed) is here — its own lever, live under
|
|
32
|
+
// the `'diff'` and `true` arms — and deliberately narrow. It never fired on the real corpus and was worth 3–5% on
|
|
33
|
+
// the money replay, so it is built to be worthless-but-harmless rather than
|
|
34
|
+
// clever: it fires only where the ledger already proves the worker holds the
|
|
35
|
+
// complete previous version, and only when the diff is smaller than the slice
|
|
36
|
+
// it would replace. A diff is a statement ABOUT bytes the worker has — assert
|
|
37
|
+
// it over bytes it never had and you have re-invented the path+hash lie in a
|
|
38
|
+
// costlier form, because a worker told "line 900 changed" about a file it only
|
|
39
|
+
// ever saw 24 KB of has no way to notice it was told a fiction.
|
|
40
|
+
|
|
41
|
+
import { createHash } from 'node:crypto';
|
|
42
|
+
import { readFileSync } from 'node:fs';
|
|
43
|
+
|
|
44
|
+
/** The delivery cap, in bytes. Equal to `NATIVE_READ_CAP` by construction — the
|
|
45
|
+
* native path's CLI display bound and this cap bound the SAME seam, so one
|
|
46
|
+
* number keeps them from double-truncating each other (see planrun's wiring). */
|
|
47
|
+
export const READ_SHIM_CAP = 24 * 1024;
|
|
48
|
+
|
|
49
|
+
// ── THE ARMS (Phase 2 pre-registration, 2026-08-18) ────────────────────────
|
|
50
|
+
// The frozen battery names four arms — A0 off, A1 cap+pointer+slice+G1, A2 diff
|
|
51
|
+
// only, A3 everything — and a single boolean can express exactly two of them.
|
|
52
|
+
// So `readShim` is an ARM NAME that still accepts the two booleans it used to:
|
|
53
|
+
// `false` is A0 and `true` is A3, both byte-identical to what they meant before
|
|
54
|
+
// this file learned the word "arm", because a flag whose meaning shifts under a
|
|
55
|
+
// caller written against the old spelling is a silent treatment change.
|
|
56
|
+
//
|
|
57
|
+
// Every lever is a separate field rather than a level on a dial, because the
|
|
58
|
+
// arms are not nested: A2 is not "less of" A1, it is a different lever entirely.
|
|
59
|
+
//
|
|
60
|
+
// Why A2 carries neither the pointer nor G1, stated so it can be argued with
|
|
61
|
+
// rather than discovered in a result:
|
|
62
|
+
// - POINTER: the prereg's table lists "pointer" inside A1 and A2 as "diff
|
|
63
|
+
// only". The pointer is the lever that answers the 48% of reads that
|
|
64
|
+
// re-read an UNCHANGED file — the single biggest saving in the replay. Fold
|
|
65
|
+
// it into A2 and A2 stops being the diff's own read: A2-vs-A1 would compare
|
|
66
|
+
// (diff + the big saving) against (cap + the big saving) and attribute the
|
|
67
|
+
// shared term to neither. So an unchanged re-read under A2 re-delivers.
|
|
68
|
+
// - G1: G1 exists because a CAP blinds a worker that cannot aim — it forces
|
|
69
|
+
// the retrieval pair onto any step granting `read`. A2 caps nothing, so
|
|
70
|
+
// there is nothing to be blind about, and firing it would make A2 a
|
|
71
|
+
// different treatment than the prereg names (a narrower admissible plan
|
|
72
|
+
// space, on top of the diff). An admission rule is not free.
|
|
73
|
+
/** @typedef {{on: boolean, cap: boolean, pointer: boolean, diff: boolean, g1: boolean}} ReadShimArm */
|
|
74
|
+
/** @type {ReadShimArm} */
|
|
75
|
+
const ARM_OFF = Object.freeze({ on: false, cap: false, pointer: false, diff: false, g1: false });
|
|
76
|
+
/** @type {ReadShimArm} */
|
|
77
|
+
const ARM_CAP = Object.freeze({ on: true, cap: true, pointer: true, diff: false, g1: true });
|
|
78
|
+
/** @type {ReadShimArm} */
|
|
79
|
+
const ARM_DIFF = Object.freeze({ on: true, cap: false, pointer: false, diff: true, g1: false });
|
|
80
|
+
/** @type {ReadShimArm} */
|
|
81
|
+
const ARM_ALL = Object.freeze({ on: true, cap: true, pointer: true, diff: true, g1: true });
|
|
82
|
+
|
|
83
|
+
/** The legal spellings, in one place so an error message cannot drift from the
|
|
84
|
+
* set it describes. */
|
|
85
|
+
export const READ_SHIM_ARMS = Object.freeze(['false (A0 — off)', "'cap' (A1)", "'diff' (A2)", 'true (A3 — all levers)']);
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolve the `readShim` flag into its arm. THROWS on anything else — the BA-4
|
|
89
|
+
* param-guard class, and deliberately not a red.
|
|
90
|
+
*
|
|
91
|
+
* An unrecognised value is OPERATOR input, arriving before a single token is
|
|
92
|
+
* spent, and there is exactly one safe way to fail on it: loudly, at once. The
|
|
93
|
+
* failure this guard exists to prevent is `readShim: 'diff '` (or 'Diff', or
|
|
94
|
+
* 'cap+diff') coerced by truthiness into A3 — a battery row that RAN the wrong
|
|
95
|
+
* arm, recorded the arm it was asked for, and is invisible in the results
|
|
96
|
+
* afterwards. That is the blind-instrument class this programme keeps paying
|
|
97
|
+
* for, and it would corrupt the contrast rather than merely fail a run.
|
|
98
|
+
*
|
|
99
|
+
* A red was the alternative and is wrong here twice over: a red is a verdict on
|
|
100
|
+
* the AGENT's work (the plan, the artifact), and this is the operator's own
|
|
101
|
+
* argument; and a red would need a terminal outcome name, which is arbiter
|
|
102
|
+
* territory and out of this change's scope.
|
|
103
|
+
* @param {boolean|'cap'|'diff'|undefined} [flag]
|
|
104
|
+
* @returns {ReadShimArm}
|
|
105
|
+
*/
|
|
106
|
+
export function readShimArm(flag) {
|
|
107
|
+
if (flag === undefined || flag === false) return ARM_OFF;
|
|
108
|
+
if (flag === true) return ARM_ALL;
|
|
109
|
+
if (flag === 'cap') return ARM_CAP;
|
|
110
|
+
if (flag === 'diff') return ARM_DIFF;
|
|
111
|
+
throw new TypeError(`readShim: unknown arm ${JSON.stringify(flag)} — legal values are ${READ_SHIM_ARMS.join(', ')}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** F19 at the bound rather than the verb (the NATIVE_READ_STRATEGY precedent):
|
|
115
|
+
* a cap the worker does not know about is a worker that reads the same file
|
|
116
|
+
* three times wondering why it keeps starting over. It states the bound, what a
|
|
117
|
+
* re-read does, and the two verbs that read a symbol whole — the capability the
|
|
118
|
+
* cap makes mandatory (G1). */
|
|
119
|
+
export const READ_SHIM_STRATEGY = '\nREAD LIMIT: a shell_read of a file over 24KB returns only the next 24KB of it, followed by a notice saying which bytes you got. This is NOT the whole file. Reading the same path again continues where the last read stopped, and once you hold all of it a re-read says so instead of re-sending the bytes. To read a function IN FULL, use ctx_recall(<symbol>) then ctx_get(<pointer>) — that returns the entire function however large its file is. To locate a line, use shell_grep(<pattern>). Never try to understand a large file by reading it whole.';
|
|
120
|
+
|
|
121
|
+
/** The DIFF arm's own line. It says nothing about a 24KB limit and nothing about
|
|
122
|
+
* continuation reads, because under A2 neither exists — a persona describing
|
|
123
|
+
* machinery that is off is a lie to the worker, and a worker rationing its reads
|
|
124
|
+
* against an imaginary cap is a different treatment than the arm names.
|
|
125
|
+
*
|
|
126
|
+
* It does not repeat the steer at ctx_recall/ctx_get either: that steer is the
|
|
127
|
+
* CAP's compensation (G1's other half), and offering it here would import half
|
|
128
|
+
* of A1 into A2. The delivery itself carries `diffNotice`'s framing inline; this
|
|
129
|
+
* line exists so the first diff is not a surprise. */
|
|
130
|
+
export const READ_SHIM_DIFF_STRATEGY = '\nRE-READS: if you shell_read a file you have already been shown in full and it has changed since, the result is a unified DIFF against the copy you already hold — not the file itself. It is labelled as one. Lines starting \'-\' were removed, \'+\' were added, \' \' are unchanged context; apply it to the copy you have and you hold the current file.';
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The persona line this arm is entitled to — empty when the arm changes nothing
|
|
134
|
+
* the worker can perceive. One arm, one line: the two are never concatenated,
|
|
135
|
+
* because a cap arm has no diff and a diff arm has no cap.
|
|
136
|
+
* @param {ReadShimArm} arm
|
|
137
|
+
* @returns {string}
|
|
138
|
+
*/
|
|
139
|
+
export function readShimStrategy(arm) {
|
|
140
|
+
if (arm.cap) return READ_SHIM_STRATEGY;
|
|
141
|
+
if (arm.diff) return READ_SHIM_DIFF_STRATEGY;
|
|
142
|
+
return '';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** A slice that ends the file needs no steer — the worker has it all now. */
|
|
146
|
+
const doneNotice = (start, total) =>
|
|
147
|
+
`\n\n[bareloop: bytes ${start}-${total} of ${total} shown — the rest of this file, which you had not seen. You now hold all of it.]`;
|
|
148
|
+
|
|
149
|
+
/** The capped-slice notice. TRUSTED framing (the `bareloop:` prefix, the same
|
|
150
|
+
* register as the native truncation notice): a worker that reads this as
|
|
151
|
+
* untrusted spill goes hunting instead of aiming, which is the failure BA-17
|
|
152
|
+
* already paid for. It names the two verbs that read a symbol IN FULL, because a
|
|
153
|
+
* capped worker with no way to aim is read-blinding on purpose — G1 in
|
|
154
|
+
* `validatePlan` makes sure those verbs are actually granted. */
|
|
155
|
+
const capNotice = (start, end, total) =>
|
|
156
|
+
`\n\n[bareloop: bytes ${start}-${end} of ${total} shown — this read is capped at ${READ_SHIM_CAP} bytes. `
|
|
157
|
+
+ 'To read a specific function IN FULL use ctx_recall(<symbol>) then ctx_get(<pointer>); to find a line use shell_grep(<pattern>). '
|
|
158
|
+
+ `Reading this path again continues from byte ${end}.]`;
|
|
159
|
+
|
|
160
|
+
/** The pointer. Legal ONLY when the ledger says every current byte was handed
|
|
161
|
+
* over — the one claim this module exists to keep honest. */
|
|
162
|
+
const pointer = (path, total) =>
|
|
163
|
+
`[bareloop: ${path} is unchanged since you read it — you already hold all ${total} bytes of it. Nothing new to show.]`;
|
|
164
|
+
|
|
165
|
+
/** The diff's own TRUSTED framing, same `bareloop:` register as the notices
|
|
166
|
+
* above. It says what the payload IS (a diff, not the file) and what it is
|
|
167
|
+
* against (the copy the worker already holds), because a worker that reads a
|
|
168
|
+
* hunk as the file's contents has been read-blinded by a saving. */
|
|
169
|
+
const diffNotice = (path, total) =>
|
|
170
|
+
`[bareloop: ${path} changed since you read it. Below is a DIFF against the version you already hold IN FULL — not the file. `
|
|
171
|
+
+ "Lines starting '-' were removed, '+' were added, ' ' are unchanged context; '@@ -a +b @@' gives the line numbers in the old and new file. "
|
|
172
|
+
+ `Apply it to the copy you have and you hold the current ${total} bytes; there is nothing else to read.]`;
|
|
173
|
+
|
|
174
|
+
/** Lines of unchanged context on each side of a hunk — enough to place the
|
|
175
|
+
* change without re-sending the file around it. */
|
|
176
|
+
const DIFF_CONTEXT = 2;
|
|
177
|
+
|
|
178
|
+
/** The LCS table is old-lines × new-lines cells. Bounded because this runs
|
|
179
|
+
* INSIDE a tool call the worker is waiting on: a 20k-line file rewritten whole
|
|
180
|
+
* is 400M cells, which is a stall and a heap spike in exchange for a diff that
|
|
181
|
+
* would blow the size bound anyway. Over the bound we decline and re-deliver —
|
|
182
|
+
* the same answer, reached without the arithmetic. */
|
|
183
|
+
const DIFF_CELL_BOUND = 1_000_000;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* A line-based diff of `oldText` → `newText`, or null if it cannot be produced
|
|
187
|
+
* within `maxBytes`. Stdlib only, and deliberately not byte-compatible with
|
|
188
|
+
* GNU `diff -u`: hunk headers here are plain 1-based `-start,count +start,count`
|
|
189
|
+
* with no attempt to match GNU's empty-range convention, because the reader is
|
|
190
|
+
* a model reading prose, not `patch`.
|
|
191
|
+
*
|
|
192
|
+
* The byte budget is enforced WHILE rendering, not checked afterwards, so an
|
|
193
|
+
* enormous diff is abandoned rather than built and then thrown away.
|
|
194
|
+
* @param {string} oldText
|
|
195
|
+
* @param {string} newText
|
|
196
|
+
* @param {number} maxBytes
|
|
197
|
+
* @returns {string|null}
|
|
198
|
+
*/
|
|
199
|
+
function lineDiff(oldText, newText, maxBytes) {
|
|
200
|
+
const a = oldText.split('\n');
|
|
201
|
+
const b = newText.split('\n');
|
|
202
|
+
// Common head and tail come off first: real edits touch a fraction of a file,
|
|
203
|
+
// and trimming is what keeps the quadratic middle small enough to be legal.
|
|
204
|
+
let head = 0;
|
|
205
|
+
while (head < a.length && head < b.length && a[head] === b[head]) head++;
|
|
206
|
+
let tail = 0;
|
|
207
|
+
while (tail < a.length - head && tail < b.length - head
|
|
208
|
+
&& a[a.length - 1 - tail] === b[b.length - 1 - tail]) tail++;
|
|
209
|
+
const ar = a.slice(head, a.length - tail);
|
|
210
|
+
const br = b.slice(head, b.length - tail);
|
|
211
|
+
if (ar.length * br.length > DIFF_CELL_BOUND) return null;
|
|
212
|
+
|
|
213
|
+
// Longest common subsequence over the changed middle, classic DP.
|
|
214
|
+
const w = br.length + 1;
|
|
215
|
+
const dp = new Uint32Array((ar.length + 1) * w);
|
|
216
|
+
for (let i = ar.length - 1; i >= 0; i--) {
|
|
217
|
+
for (let j = br.length - 1; j >= 0; j--) {
|
|
218
|
+
dp[i * w + j] = ar[i] === br[j]
|
|
219
|
+
? dp[(i + 1) * w + j + 1] + 1
|
|
220
|
+
: Math.max(dp[(i + 1) * w + j], dp[i * w + j + 1]);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/** @type {{t: string, line: string, ai: number, bi: number}[]} */
|
|
224
|
+
const rows = [];
|
|
225
|
+
let ai = 0, bi = 0;
|
|
226
|
+
const push = (/** @type {string} */ t, /** @type {string} */ line) => {
|
|
227
|
+
rows.push({ t, line, ai, bi });
|
|
228
|
+
if (t !== '+') ai++;
|
|
229
|
+
if (t !== '-') bi++;
|
|
230
|
+
};
|
|
231
|
+
for (let k = 0; k < head; k++) push(' ', a[k]);
|
|
232
|
+
let i = 0, j = 0;
|
|
233
|
+
while (i < ar.length && j < br.length) {
|
|
234
|
+
if (ar[i] === br[j]) { push(' ', ar[i]); i++; j++; }
|
|
235
|
+
else if (dp[(i + 1) * w + j] >= dp[i * w + j + 1]) { push('-', ar[i]); i++; }
|
|
236
|
+
else { push('+', br[j]); j++; }
|
|
237
|
+
}
|
|
238
|
+
while (i < ar.length) { push('-', ar[i]); i++; }
|
|
239
|
+
while (j < br.length) { push('+', br[j]); j++; }
|
|
240
|
+
for (let k = a.length - tail; k < a.length; k++) push(' ', a[k]);
|
|
241
|
+
|
|
242
|
+
// Group the changed rows into hunks, each padded with context; two changes
|
|
243
|
+
// closer than twice the context share one hunk rather than repeating lines.
|
|
244
|
+
const changed = rows.map((r, k) => (r.t === ' ' ? -1 : k)).filter((k) => k >= 0);
|
|
245
|
+
if (changed.length === 0) return null;
|
|
246
|
+
/** @type {[number, number][]} */
|
|
247
|
+
const hunks = [];
|
|
248
|
+
for (const k of changed) {
|
|
249
|
+
const last = hunks[hunks.length - 1];
|
|
250
|
+
if (last && k - last[1] <= DIFF_CONTEXT * 2 + 1) last[1] = k;
|
|
251
|
+
else hunks.push([k, k]);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
let out = '';
|
|
255
|
+
let bytes = 0;
|
|
256
|
+
const emit = (/** @type {string} */ s) => {
|
|
257
|
+
bytes += Buffer.byteLength(s, 'utf8');
|
|
258
|
+
out += s;
|
|
259
|
+
return bytes <= maxBytes;
|
|
260
|
+
};
|
|
261
|
+
for (const [lo, hi] of hunks) {
|
|
262
|
+
const from = Math.max(0, lo - DIFF_CONTEXT);
|
|
263
|
+
const to = Math.min(rows.length - 1, hi + DIFF_CONTEXT);
|
|
264
|
+
let aCount = 0, bCount = 0;
|
|
265
|
+
for (let k = from; k <= to; k++) {
|
|
266
|
+
if (rows[k].t !== '+') aCount++;
|
|
267
|
+
if (rows[k].t !== '-') bCount++;
|
|
268
|
+
}
|
|
269
|
+
if (!emit(`@@ -${rows[from].ai + 1},${aCount} +${rows[from].bi + 1},${bCount} @@\n`)) return null;
|
|
270
|
+
for (let k = from; k <= to; k++) if (!emit(`${rows[k].t}${rows[k].line}\n`)) return null;
|
|
271
|
+
}
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** A UTF-8 sequence split across a slice boundary decodes to a replacement
|
|
276
|
+
* character on BOTH sides — the byte is then delivered twice and read as
|
|
277
|
+
* garbage once. Walk back to the last lead byte instead. `end` moves by at most
|
|
278
|
+
* 3, which no cap this module ships can drive to zero progress. */
|
|
279
|
+
function utf8Boundary(buf, end) {
|
|
280
|
+
if (end >= buf.length) return buf.length;
|
|
281
|
+
let e = end;
|
|
282
|
+
while (e > 0 && (buf[e] & 0xc0) === 0x80) e--;
|
|
283
|
+
return e;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** The stale-serve's TRUSTED framing, same `bareloop:` register as the notices
|
|
287
|
+
* above. Everything it has to say is load-bearing and none of it can be dropped
|
|
288
|
+
* for brevity:
|
|
289
|
+
* - the pointer is STALE, so the worker knows why it is not getting a chunk;
|
|
290
|
+
* - the range may now hold DIFFERENT code than the symbol it asked for. This
|
|
291
|
+
* is the sentence that makes the serve legal at all. litectx refuses to
|
|
292
|
+
* slice a drifted range itself, and says why in `StalePointerError`'s own
|
|
293
|
+
* docstring: an UNLABELLED line slice can silently be another symbol's body,
|
|
294
|
+
* which is worse than an exception. Labelled, it is a raw slice the worker
|
|
295
|
+
* can judge; unlabelled it would be the same lie the ledger exists to stop;
|
|
296
|
+
* - which lines it actually got, and how long the file is now;
|
|
297
|
+
* - that NOTHING here was recorded as delivered, so the worker cannot read the
|
|
298
|
+
* serve as progress against the read seam's cap;
|
|
299
|
+
* - how to get a real pointer back.
|
|
300
|
+
* No `]` appears before the end: the bracket is the notice's own terminator. */
|
|
301
|
+
const staleServeNotice = (path, startLine, shownEnd, lines, cut, cap) =>
|
|
302
|
+
`[bareloop: the ctx_get pointer for ${path} lines ${startLine}-${shownEnd} is STALE — that file changed on disk after it was indexed, `
|
|
303
|
+
+ 'so the recorded line range may now hold DIFFERENT code than the symbol you asked for. '
|
|
304
|
+
+ `Rather than nothing, below are lines ${startLine}-${shownEnd} of ${path} exactly as they stand on disk right now `
|
|
305
|
+
+ `(0-based line indexes, as ctx_recall prints them; the file has ${lines} lines, so its last index is ${lines - 1})`
|
|
306
|
+
+ `${cut ? `, cut at the ${cap}-byte read limit` : ''}. `
|
|
307
|
+
+ 'This is a raw line slice — not a chunk, not the whole file — and none of it was recorded as delivered to you, '
|
|
308
|
+
+ 'so a shell_read of this path is unaffected. Re-run ctx_recall(<symbol>) for a fresh pointer.]';
|
|
309
|
+
|
|
310
|
+
/** The two refusals. Both are RESULTS the worker reads, never throws — a stale
|
|
311
|
+
* pointer on a vanished or shrunken file is ordinary worker feedback, and a
|
|
312
|
+
* throw here would look exactly like a verb that was never reached (the F18
|
|
313
|
+
* blindness rule that put `emit` in `createCtxTools` in the first place). */
|
|
314
|
+
const staleAbsentNotice = (path, startLine, endLine, detail) =>
|
|
315
|
+
`[bareloop: the ctx_get pointer for ${path} lines ${startLine}-${endLine} is stale (${detail}), `
|
|
316
|
+
+ `and ${path} is no longer on disk — there is nothing to show you. Re-run ctx_recall(<symbol>) for a fresh pointer.]`;
|
|
317
|
+
|
|
318
|
+
const stalePastEofNotice = (path, startLine, endLine, lines, detail) =>
|
|
319
|
+
`[bareloop: the ctx_get pointer for ${path} lines ${startLine}-${endLine} is stale (${detail}), `
|
|
320
|
+
+ `and that file ${lines === 0 ? 'is now empty' : `is now only ${lines} lines long, so its last 0-based index is ${lines - 1}`}, `
|
|
321
|
+
+ 'so that range is entirely past its end — there is nothing to show you. '
|
|
322
|
+
+ 'Re-run ctx_recall(<symbol>) for a fresh pointer.]';
|
|
323
|
+
|
|
324
|
+
/** @typedef {{file: string, path: string, startLine: number, endLine: number, detail: string}} StaleReq */
|
|
325
|
+
/** @typedef {{text: string, outcome: 'stale-served'|'stale-absent'|'stale-past-eof'}} StaleServe */
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Create the shim: one delivery ledger, and the two seams that speak for it.
|
|
329
|
+
*
|
|
330
|
+
* `wrapRead` is the read tool's wrapper (the ledger's only writer). `serveStale`
|
|
331
|
+
* is the answer to a stale `ctx_get` pointer, and it deliberately holds NO
|
|
332
|
+
* reference to the ledger — see its own note. Both live in one object because
|
|
333
|
+
* they are one worker's shim: the read seam and the retrieval seam the cap
|
|
334
|
+
* steers a capped worker towards.
|
|
335
|
+
* @param {{cap?: number, arm?: ReadShimArm}} [opts] `cap`: delivery bound in bytes (the
|
|
336
|
+
* shipped value is `READ_SHIM_CAP`; the option exists so the native path can pin the
|
|
337
|
+
* two bounds to one number rather than two constants drifting apart). `arm`: which
|
|
338
|
+
* levers are live (`readShimArm`). Defaults to ALL of them, so every caller written
|
|
339
|
+
* before the arms existed keeps the behaviour it was written against.
|
|
340
|
+
*/
|
|
341
|
+
export function createReadShim({ cap = READ_SHIM_CAP, arm = ARM_ALL } = {}) {
|
|
342
|
+
/** path → what the worker has been handed of the CURRENT content. `full` is
|
|
343
|
+
* the delivered text itself, kept ONLY while the worker holds the whole file
|
|
344
|
+
* (null otherwise) because that is the only state a diff is legal from.
|
|
345
|
+
*
|
|
346
|
+
* MEMORY COST, stated rather than waved at: one copy of every fully-delivered
|
|
347
|
+
* file, for the lifetime of one worker. The ceiling is real but it is not a
|
|
348
|
+
* new one — those exact bytes are already sitting in the worker's transcript,
|
|
349
|
+
* where they are the expensive copy (they are billed every round; this one is
|
|
350
|
+
* billed never). A partly-delivered file stores nothing, so the pathological
|
|
351
|
+
* case — a worker paging through many huge files — is precisely the case that
|
|
352
|
+
* holds no copies. The map dies with the worker along with the rest of the
|
|
353
|
+
* ledger; nothing here is process-wide.
|
|
354
|
+
* @type {Map<string, {hash: string, total: number, delivered: number, full: string|null}>} */
|
|
355
|
+
const ledger = new Map();
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* THE STALE-POINTER SERVE. When `ctx_get` throws `StalePointerError` — the file
|
|
359
|
+
* changed on disk after indexing — hand back the REQUESTED LINE RANGE read
|
|
360
|
+
* fresh from disk, labelled, instead of nothing.
|
|
361
|
+
*
|
|
362
|
+
* WHY IT EXISTS: the cap's own strategy line steers a capped worker at
|
|
363
|
+
* `ctx_recall` → `ctx_get` for a whole function. A worker that has just EDITED
|
|
364
|
+
* the file it is working on — the normal case — then gets a stale pointer and
|
|
365
|
+
* zero bytes. Measured on the A1 arm's three green runs: 10 `ctx_get` calls, 5
|
|
366
|
+
* stale, 0 bytes each, every one a file the worker had just written. The round
|
|
367
|
+
* is paid for and returns nothing, and the steer is what walked it there.
|
|
368
|
+
*
|
|
369
|
+
* WHY IT NEVER TOUCHES THE LEDGER, and why that is the whole design: the
|
|
370
|
+
* ledger's `full` is the licence for the pointer response ("you already hold
|
|
371
|
+
* all of it"), and it is held ONLY while `delivered === total`. A ranged serve
|
|
372
|
+
* hands over a SLICE — of a file the read seam may never have delivered a byte
|
|
373
|
+
* of, from the MIDDLE rather than the next unseen prefix. Record it and the
|
|
374
|
+
* next read answers "unchanged, you already have it" to a worker holding 90
|
|
375
|
+
* lines of 900: exactly the untruthful-pointer failure this module was built
|
|
376
|
+
* to prevent (250 of them in the $0 replay, median 73,348 bytes hidden). So
|
|
377
|
+
* `ledger` is not in scope here — not "we are careful not to write it", but no
|
|
378
|
+
* reference at all — and the cost is that a later `shell_read` may re-deliver
|
|
379
|
+
* bytes the serve already showed. That is the fail-safe direction: the shim
|
|
380
|
+
* over-delivers rather than lies.
|
|
381
|
+
*
|
|
382
|
+
* Gated on the CAP lever, not on `arm.on`: the serve is the cap's own
|
|
383
|
+
* compensation (the same argument G1 and `READ_SHIM_STRATEGY` are made of). An
|
|
384
|
+
* arm that caps nothing never steered the worker at `ctx_get` and must keep
|
|
385
|
+
* `ctx_get`'s untouched contract.
|
|
386
|
+
*
|
|
387
|
+
* Never throws — a vanished or shrunken file is a refusal RESULT.
|
|
388
|
+
* @param {StaleReq} req `file` is the ABSOLUTE path the gate already judged
|
|
389
|
+
* for this call; `path` is the repo-relative spelling shown to the worker.
|
|
390
|
+
* @returns {StaleServe|null} null when this arm carries no cap
|
|
391
|
+
*/
|
|
392
|
+
const serveStale = ({ file, path, startLine, endLine, detail }) => {
|
|
393
|
+
if (!arm.cap) return null;
|
|
394
|
+
// THE LINE SPACE, verified against real litectx rather than read off a
|
|
395
|
+
// comment: `ctx_recall`/`ctx_get` handles are 0-BASED and INCLUSIVE (a probe
|
|
396
|
+
// indexed a file, took recall's pointer, and matched `get`'s text against
|
|
397
|
+
// `lines.slice(start, end + 1)` — the 1-based reading returned the wrong two
|
|
398
|
+
// lines). The worker's numbers are echoed back in that same space, because
|
|
399
|
+
// `LINE_SPACE` already told it that is the space it is in, and a serve that
|
|
400
|
+
// silently renumbered would be the off-by-one that doc exists to prevent.
|
|
401
|
+
const a = Math.max(0, Math.floor(Number(startLine) || 0));
|
|
402
|
+
const b = Math.max(a, Math.floor(Number(endLine) || a));
|
|
403
|
+
let content;
|
|
404
|
+
try {
|
|
405
|
+
content = readFileSync(file, 'utf8');
|
|
406
|
+
} catch {
|
|
407
|
+
return { text: staleAbsentNotice(path, a, b, detail), outcome: 'stale-absent' };
|
|
408
|
+
}
|
|
409
|
+
const lines = content.split('\n');
|
|
410
|
+
// A trailing newline yields a final empty element that is not a line.
|
|
411
|
+
if (lines.length && lines[lines.length - 1] === '') lines.pop();
|
|
412
|
+
if (a >= lines.length) {
|
|
413
|
+
return { text: stalePastEofNotice(path, a, b, lines.length, detail), outcome: 'stale-past-eof' };
|
|
414
|
+
}
|
|
415
|
+
const shownEnd = Math.min(b, lines.length - 1);
|
|
416
|
+
let body = `${lines.slice(a, shownEnd + 1).join('\n')}\n`;
|
|
417
|
+
// The same cap as every other delivery this module makes, on the same
|
|
418
|
+
// UTF-8-safe boundary. A requested range is worker-chosen and unbounded.
|
|
419
|
+
const buf = Buffer.from(body, 'utf8');
|
|
420
|
+
const cut = buf.length > cap;
|
|
421
|
+
if (cut) body = buf.subarray(0, utf8Boundary(buf, cap)).toString('utf8');
|
|
422
|
+
return {
|
|
423
|
+
text: `${staleServeNotice(path, a, shownEnd, lines.length, cut, cap)}\n\n${body}`,
|
|
424
|
+
outcome: 'stale-served',
|
|
425
|
+
};
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Wrap a read tool with the delivery ledger. MUTATES `tool.execute` and returns
|
|
430
|
+
* the tool, which is safe because the caller builds fresh tool objects per
|
|
431
|
+
* worker (`createShellTools()` inside `mkWorker`) — so the ledger's lifetime is
|
|
432
|
+
* one worker, matching the fresh-Loop-per-step reset the replay segmented on.
|
|
433
|
+
* A process-wide ledger would carry one step's coverage into the next step's
|
|
434
|
+
* transcript, which never held those bytes.
|
|
435
|
+
* @param {{name: string, execute: (args: any) => Promise<any>}} tool the read tool
|
|
436
|
+
* @returns {{name: string, execute: (args: any) => Promise<any>}} the same tool
|
|
437
|
+
*/
|
|
438
|
+
const wrapRead = (tool) => {
|
|
439
|
+
// An OFF arm wraps nothing at all — not a wrapper that passes through, which
|
|
440
|
+
// would still be a `tool.execute` this module owns. A0 has to be the seam
|
|
441
|
+
// untouched, and "untouched" is the assertion, not "behaves the same".
|
|
442
|
+
if (!arm.on) return tool;
|
|
443
|
+
const inner = tool.execute;
|
|
444
|
+
tool.execute = async (/** @type {any} */ args) => {
|
|
445
|
+
const path = typeof args?.path === 'string' ? args.path : null;
|
|
446
|
+
const r = await inner(args);
|
|
447
|
+
// Anything that is not text (or a call with no path to key on) is the tool's
|
|
448
|
+
// own business — pass it through and record nothing. A throw never reaches
|
|
449
|
+
// here at all: the worker gets the failure, and the ledger stays clean of a
|
|
450
|
+
// path nobody ever received bytes for.
|
|
451
|
+
if (path === null || typeof r !== 'string') return r;
|
|
452
|
+
|
|
453
|
+
const buf = Buffer.from(r, 'utf8');
|
|
454
|
+
const total = buf.length;
|
|
455
|
+
const hash = createHash('sha256').update(buf).digest('hex');
|
|
456
|
+
const seen = ledger.get(path);
|
|
457
|
+
// Same content as last time — and only then does coverage mean anything.
|
|
458
|
+
// Any change (grown, shrunk, edited) resets to zero: coverage that survives
|
|
459
|
+
// a content change is the masking direction, because a file that shrank
|
|
460
|
+
// under stale coverage would answer as "fully delivered".
|
|
461
|
+
//
|
|
462
|
+
// Without the CAP lever (A2) there is no such thing as partial coverage: a
|
|
463
|
+
// read hands over the file, so `start` is always 0 and `end` is always the
|
|
464
|
+
// end. Everything below then falls out unchanged — the slice IS the file,
|
|
465
|
+
// and the diff's budget below is the whole re-delivery it replaces, which is
|
|
466
|
+
// the same sentence the capped arm's budget says, at a different size.
|
|
467
|
+
const start = arm.cap && seen && seen.hash === hash ? seen.delivered : 0;
|
|
468
|
+
const end = arm.cap ? utf8Boundary(buf, Math.min(start + cap, total)) : total;
|
|
469
|
+
|
|
470
|
+
// L2, THE DIFF LEVER. Two guards, both refusals rather than adjustments:
|
|
471
|
+
// - `seen.full !== null` IS the coverage guard: the previous bytes are
|
|
472
|
+
// retained only when every one of them was delivered, so a partly-seen
|
|
473
|
+
// old version has nothing to diff against and re-delivers from 0.
|
|
474
|
+
// - the budget is the slice this diff REPLACES, `end - start` bytes of a
|
|
475
|
+
// fresh capped re-delivery. Bounding by the cap alone would let a diff
|
|
476
|
+
// be several times a small file and still ship; bounding by the slice
|
|
477
|
+
// makes "smaller than what it replaces" true by construction, and is
|
|
478
|
+
// never looser than the cap.
|
|
479
|
+
// A consequence worth naming rather than tuning away: the framing itself is
|
|
480
|
+
// ~350 bytes, so a file smaller than that can never produce a legal diff and
|
|
481
|
+
// always re-delivers whole. That is the rule working, not a gap — a diff of
|
|
482
|
+
// a 78-byte file costs more than the file.
|
|
483
|
+
//
|
|
484
|
+
// A rendered diff is always COMPLETE — the renderer returns the whole thing
|
|
485
|
+
// or null, never a prefix — which is what licenses the ledger line below:
|
|
486
|
+
// the worker holds the new content whole, so the next unchanged read is a
|
|
487
|
+
// truthful pointer. Anything less than complete falls through to the slice
|
|
488
|
+
// path and records only what that path actually handed over.
|
|
489
|
+
if (arm.diff && seen && seen.hash !== hash && seen.full !== null) {
|
|
490
|
+
const d = lineDiff(seen.full, r, Math.max(0, (end - start) - Buffer.byteLength(diffNotice(path, total), 'utf8') - 2));
|
|
491
|
+
if (d !== null) {
|
|
492
|
+
ledger.set(path, { hash, total, delivered: total, full: r });
|
|
493
|
+
return `${diffNotice(path, total)}\n\n${d}`;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// The POINTER lever. Reachable only when the ledger already covers the
|
|
498
|
+
// current content — under A2 that is an unchanged re-read of a file handed
|
|
499
|
+
// over whole, and the arm answers it by handing the bytes over again. Saying
|
|
500
|
+
// "you already have it" there would be TRUE and still wrong: it is the
|
|
501
|
+
// pointer's saving, and A2 is the diff's own read (see the arm table above).
|
|
502
|
+
//
|
|
503
|
+
// `seen.hash === hash` is spelled out rather than left implied by `start`: on a
|
|
504
|
+
// ZERO-BYTE content `start >= total` is 0 >= 0 and holds with no ledger entry at
|
|
505
|
+
// all, so the FIRST read of an empty file — and, worse, a read of a file that was
|
|
506
|
+
// just TRUNCATED to empty since the worker was handed its contents — answered
|
|
507
|
+
// "unchanged since you read it, you already hold all of it". Both are untruthful
|
|
508
|
+
// pointers on a delivery that never happened, which is the one thing this module
|
|
509
|
+
// exists to make impossible. With the guard, an empty file falls through to the
|
|
510
|
+
// slice path and is delivered (as nothing) truthfully.
|
|
511
|
+
if (arm.pointer && seen && seen.hash === hash && start >= total) {
|
|
512
|
+
ledger.set(path, { hash, total, delivered: total, full: r });
|
|
513
|
+
return pointer(path, total);
|
|
514
|
+
}
|
|
515
|
+
ledger.set(path, { hash, total, delivered: end, full: end === total ? r : null });
|
|
516
|
+
const slice = buf.subarray(start, end).toString('utf8');
|
|
517
|
+
// A first read that fits under the cap is the tool's own bytes, untouched —
|
|
518
|
+
// no notice, no reframing. That is the overwhelming majority of reads, and
|
|
519
|
+
// it keeps the shim invisible where it buys nothing.
|
|
520
|
+
if (start === 0 && end === total) return slice;
|
|
521
|
+
return slice + (end < total ? capNotice(start, end, total) : doneNotice(start, total));
|
|
522
|
+
};
|
|
523
|
+
return tool;
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
return { arm, cap, wrapRead, serveStale };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* The one-tool spelling, kept for every caller written before the shim became an
|
|
531
|
+
* object: wrap this read tool with its own private ledger and hand it back.
|
|
532
|
+
* @param {{name: string, execute: (args: any) => Promise<any>}} tool
|
|
533
|
+
* @param {{cap?: number, arm?: ReadShimArm}} [opts]
|
|
534
|
+
* @returns {{name: string, execute: (args: any) => Promise<any>}} the same tool
|
|
535
|
+
*/
|
|
536
|
+
export function wrapReadTool(tool, opts = {}) {
|
|
537
|
+
return createReadShim(opts).wrapRead(tool);
|
|
538
|
+
}
|
package/src/reuse.js
CHANGED
|
@@ -1332,6 +1332,7 @@ export function resumeTreeGate({ head, seed, dirty }) {
|
|
|
1332
1332
|
* @param {number} [opts.strikeLimit] forwarded (a plan step's strike ceiling)
|
|
1333
1333
|
* @param {number} [opts.closeTimeoutMs] forwarded
|
|
1334
1334
|
* @param {boolean} [opts.layerRoot] forwarded
|
|
1335
|
+
* @param {boolean|'cap'|'diff'} [opts.readShim] the read shim's ARM (A0/A1/A2/A3), forwarded verbatim
|
|
1335
1336
|
* @param {any} [opts.resume] RESUME (module C) — a `readResume` reading of the KILLED
|
|
1336
1337
|
* run's own spine. Its `approvalHash` must match this run's, or the run refuses:
|
|
1337
1338
|
* a resume continues ONE signed run, not any run.
|
|
@@ -1348,7 +1349,7 @@ export async function runReuse(opts) {
|
|
|
1348
1349
|
const {
|
|
1349
1350
|
job, approvals, registryDir, patient, workdir, provider, selectionProvider,
|
|
1350
1351
|
emit, ask, pinned = null, shortlist = null, forceCold = false,
|
|
1351
|
-
providerFor, nativeProvider, capRuns, strikeLimit, closeTimeoutMs, layerRoot,
|
|
1352
|
+
providerFor, nativeProvider, capRuns, strikeLimit, closeTimeoutMs, layerRoot, readShim,
|
|
1352
1353
|
runJob = shippedRunJob, now = () => Date.now(),
|
|
1353
1354
|
} = opts;
|
|
1354
1355
|
const runid = opts.runid ?? Date.now().toString(36);
|
|
@@ -1516,6 +1517,7 @@ export async function runReuse(opts) {
|
|
|
1516
1517
|
...(strikeLimit !== undefined ? { strikeLimit } : {}),
|
|
1517
1518
|
...(closeTimeoutMs !== undefined ? { closeTimeoutMs } : {}),
|
|
1518
1519
|
...(layerRoot !== undefined ? { layerRoot } : {}),
|
|
1520
|
+
...(readShim !== undefined ? { readShim } : {}),
|
|
1519
1521
|
// the fold, and whether the fold is EXACT. `priorSpendComplete` used to stop at
|
|
1520
1522
|
// the `try-start` record: it was computed, written down, and then not handed to
|
|
1521
1523
|
// the one component whose `job-end` the row's own `spendComplete` is read off —
|
package/src/run.js
CHANGED
|
@@ -16,6 +16,7 @@ import { LiteCtx } from 'litectx';
|
|
|
16
16
|
import { validateJob, jobSpecHash, checkApproval } from './job.js';
|
|
17
17
|
import { runPlan } from './planrun.js';
|
|
18
18
|
import { SMOKE_STORE, resolveHumanRuling } from './kinds.js';
|
|
19
|
+
import { readShimArm } from './readshim.js';
|
|
19
20
|
|
|
20
21
|
/** @typedef {{code: string, path: string, detail?: string}} Red */
|
|
21
22
|
|
|
@@ -156,6 +157,19 @@ async function primitiveSmoke(workdir) {
|
|
|
156
157
|
* collision walk would mint a `-2` beside the work the resume exists to continue. The
|
|
157
158
|
* fold's precedent exactly: the chain's state is DECLARED, never re-derived. Forwarded
|
|
158
159
|
* verbatim to the plan flow; absent is the cold path.
|
|
160
|
+
* @param {boolean|'cap'|'diff'} [opts.readShim=false] THE READ SHIM (src/readshim.js) —
|
|
161
|
+
* WHICH ARM to run, threaded to the plan flow. The Phase 2 pre-registration's four:
|
|
162
|
+
* `false` A0 — off (the default)
|
|
163
|
+
* `'cap'` A1 — cap + pointer + next-unseen-slice + G1, no diff
|
|
164
|
+
* `'diff'` A2 — the diff lever alone: no cap, no pointer, no G1
|
|
165
|
+
* `true` A3 — every lever
|
|
166
|
+
* Anything else THROWS below, before a token is spent: a mis-spelled arm coerced
|
|
167
|
+
* by truthiness into A3 would run one treatment under another's label and be
|
|
168
|
+
* invisible in the results afterwards.
|
|
169
|
+
* OFF is byte-identical to the pre-shim run in every observable (G1 included): the
|
|
170
|
+
* frozen A0 baseline arm has to be exactly today, so a guard firing under a disabled
|
|
171
|
+
* shim would make the baseline a treatment arm. The default-flip belongs to a
|
|
172
|
+
* paid contrast nobody has approved yet (the `layerRoot` precedent, F41).
|
|
159
173
|
* @param {boolean} [opts.layerRoot=false] Layer R (within-run ratchet) — shell
|
|
160
174
|
* territory, threaded to the plan flow. Defaults OFF
|
|
161
175
|
* (decided 2026-07-21): fixation is extinct on every current job (F41), so
|
|
@@ -187,7 +201,15 @@ async function primitiveSmoke(workdir) {
|
|
|
187
201
|
* 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'step-stalled' |
|
|
188
202
|
* 'hitl-pause' | 'hitl-decision-red' | `step-red:<id>`
|
|
189
203
|
*/
|
|
190
|
-
export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider = null, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, layerRoot = false, bridge = null, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null, heldRuling = null, reviewDoor = null, doorRerun = null }) {
|
|
204
|
+
export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider = null, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, layerRoot = false, readShim = false, bridge = null, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null, heldRuling = null, reviewDoor = null, doorRerun = null }) {
|
|
205
|
+
// THE READ SHIM's ARM, resolved at the door — the FIRST thing this entry does,
|
|
206
|
+
// before the ledger, before the approval gate, before a byte of the spec is read.
|
|
207
|
+
// An unrecognised spelling throws here at zero cost instead of being coerced by
|
|
208
|
+
// truthiness into A3 and running a whole paid battery row under an arm label it
|
|
209
|
+
// never executed — a wrong result that looks like a fine one, which is the exact
|
|
210
|
+
// blind-instrument failure this programme keeps logging. Only the guard runs
|
|
211
|
+
// here; the flag itself is threaded onward to the plan flow as written.
|
|
212
|
+
readShimArm(readShim);
|
|
191
213
|
// 0. the ledger's counters, declared FIRST so that every job-end — including
|
|
192
214
|
// the pre-token reds below — can state a real figure. An omitted `spentUsd` is
|
|
193
215
|
// not a zero: a consumer reads `undefined` and either crashes or launders it
|
|
@@ -356,7 +378,7 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
356
378
|
// accounts it natively (F12) and the job-end money contract is unchanged.
|
|
357
379
|
{
|
|
358
380
|
const outcome = await runPlan(job, {
|
|
359
|
-
workdir, provider, nativeProvider, providerFor, judgeProvider, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, layerRoot, bridge, priorWallMs: chainWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun, priorSpentUsd: chainFoldUsd,
|
|
381
|
+
workdir, provider, nativeProvider, providerFor, judgeProvider, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, layerRoot, readShim, bridge, priorWallMs: chainWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun, priorSpentUsd: chainFoldUsd,
|
|
360
382
|
remainingUsd: () => Math.min(shellCapUsd, job.budgetUsd - spentUsd),
|
|
361
383
|
isUnpriced: () => unpriced, // F6: let the plan flow bail in-flight, not just after it returns
|
|
362
384
|
spendComplete, // …and let its money-halt readout say whether the remaining it quotes is exact
|