bareloop 0.33.0 → 0.35.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 +47 -0
- package/bareloop.context.md +263 -27
- package/package.json +1 -1
- package/src/bundlerun.js +13 -4
- package/src/draftspend.js +82 -0
- package/src/exits.js +10 -1
- package/src/interviewrun.js +10 -9
- package/src/job.js +39 -2
- package/src/judged.js +25 -5
- package/src/kinds.js +2 -0
- package/src/ledger.js +104 -9
- package/src/legs.js +171 -0
- package/src/monthly.js +133 -87
- package/src/panel/authorroutes.js +95 -15
- package/src/panel/authorsession.js +408 -113
- package/src/panel/importroutes.js +359 -0
- package/src/panel/importrun.js +203 -0
- package/src/panel/index.html +1071 -184
- package/src/panel/runroutes.js +219 -0
- package/src/panel/server.js +855 -81
- package/src/panel/status.js +51 -0
- package/src/planrun.js +63 -31
- package/src/providerrows.js +21 -2
- package/src/providers.js +4 -4
- package/src/ralph.js +3 -0
- package/src/replay.js +139 -19
- package/src/replayio.js +11 -4
- package/src/reuse.js +20 -3
- package/src/run.js +6 -3
- package/src/runlist.js +90 -7
- package/src/text.js +20 -0
- package/src/u-readout.js +9 -1
- package/src/u-watchdog.mjs +4 -2
- package/src/userrun.js +166 -65
- package/types/draftspend.d.ts +53 -0
- package/types/job.d.ts +25 -0
- package/types/judged.d.ts +5 -2
- package/types/ledger.d.ts +44 -0
- package/types/legs.d.ts +103 -0
- package/types/monthly.d.ts +39 -22
- package/types/panel/authorroutes.d.ts +38 -1
- package/types/panel/authorsession.d.ts +109 -2
- package/types/panel/importroutes.d.ts +129 -0
- package/types/panel/importrun.d.ts +77 -0
- package/types/panel/runroutes.d.ts +32 -0
- package/types/panel/server.d.ts +255 -2
- package/types/panel/status.d.ts +51 -0
- package/types/planrun.d.ts +13 -21
- package/types/providerrows.d.ts +14 -1
- package/types/providers.d.ts +1 -2
- package/types/replay.d.ts +25 -1
- package/types/replayio.d.ts +2 -2
- package/types/reuse.d.ts +5 -0
- package/types/run.d.ts +6 -2
- package/types/runlist.d.ts +66 -2
- package/types/text.d.ts +19 -0
package/src/replayio.js
CHANGED
|
@@ -87,7 +87,10 @@ export function resolveSiblings(spinePath) {
|
|
|
87
87
|
const base = basename(spinePath);
|
|
88
88
|
const stem = base.replace(/\.jsonl$/, '');
|
|
89
89
|
const runId = stem.startsWith('u-') ? stem.slice(2) : stem;
|
|
90
|
-
|
|
90
|
+
let auditPath = join(dir, `${stem}-gate-audit.jsonl`);
|
|
91
|
+
// a bundle run (`<bundle>/runs/<runid>/spine.jsonl`) keeps its tool log beside the spine as plain `gate-audit.jsonl`
|
|
92
|
+
const bundleAudit = join(dir, 'gate-audit.jsonl');
|
|
93
|
+
if (base === 'spine.jsonl' && !existsSync(auditPath) && existsSync(bundleAudit)) auditPath = bundleAudit;
|
|
91
94
|
return { runId, auditPath: existsSync(auditPath) ? auditPath : null };
|
|
92
95
|
}
|
|
93
96
|
|
|
@@ -96,7 +99,7 @@ export function resolveSiblings(spinePath) {
|
|
|
96
99
|
* hand both to {@link replayRun}. Read-only, $0, mints no verdict.
|
|
97
100
|
* @param {string} spinePath absolute or cwd-relative path to a `.jsonl` spine
|
|
98
101
|
* @param {{preParsedSpine?: {records: any[], skipped: number}, skipAudit?: boolean,
|
|
99
|
-
* auditPathOverride?: string|null}} [opts]
|
|
102
|
+
* auditPathOverride?: string|string[]|null}} [opts]
|
|
100
103
|
* `preParsedSpine`: avoids a second parse of the same file when the caller
|
|
101
104
|
* ({@link listSpines}) already read it once to run {@link looksLikeSpine}.
|
|
102
105
|
* `skipAudit` (PR #23 review item 5, 2026-08-26, carried over): a
|
|
@@ -120,13 +123,17 @@ export function replayOne(spinePath, opts = {}) {
|
|
|
120
123
|
const { runId, auditPath: resolvedAuditPath } = resolveSiblings(spinePath);
|
|
121
124
|
const auditPath = auditPathOverride !== undefined ? auditPathOverride : resolvedAuditPath;
|
|
122
125
|
const spine = preParsedSpine ?? parseJsonl(spinePath);
|
|
123
|
-
|
|
126
|
+
// P5-R: a RESUMED run still being written has TWO homes for its tool log — the finished legs' file and the
|
|
127
|
+
// live leg's file in the patient tree — so a caller may hand every path the run's log is in
|
|
128
|
+
const auditPaths = Array.isArray(auditPath) ? auditPath : (auditPath ? [auditPath] : []);
|
|
129
|
+
const audit = { records: /** @type {any[]} */ ([]), skipped: 0 };
|
|
130
|
+
if (!skipAudit) for (const ap of auditPaths) { const part = parseJsonl(ap); audit.records.push(...part.records); audit.skipped += part.skipped; }
|
|
124
131
|
// `auditAvailable`: true only when a sidecar was actually FOUND AND READ.
|
|
125
132
|
// `skipAudit` (a deliberate cheap directory listing) and "no sidecar on
|
|
126
133
|
// disk" both mean this call never learned the real tool-call count, so
|
|
127
134
|
// both report unknown, never a coincidental 0 (doctrine: unknown reported
|
|
128
135
|
// as unknown, never rendered as zero — see `replayRun`'s own doc).
|
|
129
|
-
const auditAvailable = !skipAudit &&
|
|
136
|
+
const auditAvailable = !skipAudit && auditPaths.length > 0;
|
|
130
137
|
const summary = replayRun(spine.records, audit.records, { runId, auditAvailable });
|
|
131
138
|
summary.skipped += spine.skipped + audit.skipped;
|
|
132
139
|
return summary;
|
package/src/reuse.js
CHANGED
|
@@ -52,6 +52,7 @@ import { closeStagesOf } from './plan.js';
|
|
|
52
52
|
import { MIN_WALL_MS, jobSpecHash } from './job.js';
|
|
53
53
|
import { isObj, isNonEmptyString } from './validate.js';
|
|
54
54
|
import { readGrade } from './trend.js';
|
|
55
|
+
import { legsOf } from './legs.js';
|
|
55
56
|
|
|
56
57
|
/** the paused-run TERMINAL and its spine record share one name (`runPlan` returns
|
|
57
58
|
* it and `runJob` writes it onto `job-end`), spelled once in `declaredclose.js`
|
|
@@ -117,6 +118,11 @@ export const REUSE_GRADED_RED = Object.freeze(['escalated']);
|
|
|
117
118
|
* allowances the loop's remaining tries are meant to spend against, and the human
|
|
118
119
|
* ones are the stop no further try can change (see `hardStop`).
|
|
119
120
|
*
|
|
121
|
+
* `stopped` (PANEL-BUILD.md P5 item 5, hamr 2026-10-02): the PERSON ended the leg at a round
|
|
122
|
+
* boundary (a `<spine>.stop` request). Nothing was judged and nothing failed — finished steps stand on
|
|
123
|
+
* disk, the allowance is unspent, so it is a checkpoint exactly like a cap-halt. Arbiter-adjacent (it
|
|
124
|
+
* widens what resume may re-enter); ruled by hamr, not chosen by a builder.
|
|
125
|
+
*
|
|
120
126
|
* `provider-red` folded in 2026-08-25 (PRD v1.80 TODO #4, F115 "Ruled: one transport
|
|
121
127
|
* retry"; hamr's ruling verbatim: *"joins resume anyways and you decide but you are
|
|
122
128
|
* given an honest readout and with 1 retry"*). It is a MACHINE halt, not a human one
|
|
@@ -139,7 +145,7 @@ export const REUSE_GRADED_RED = Object.freeze(['escalated']);
|
|
|
139
145
|
* does not end up with two lists that can drift, which is the exact failure this
|
|
140
146
|
* constant exists to end (see the paragraph above).
|
|
141
147
|
*/
|
|
142
|
-
export const CHECKPOINT_OUTCOMES = Object.freeze(['cap-halt', 'wall-halt', 'step-stalled', 'provider-red', ...HUMAN_CHECKPOINTS]);
|
|
148
|
+
export const CHECKPOINT_OUTCOMES = Object.freeze(['cap-halt', 'wall-halt', 'step-stalled', 'provider-red', 'stopped', ...HUMAN_CHECKPOINTS]);
|
|
143
149
|
|
|
144
150
|
/**
|
|
145
151
|
* v1's definition of "the same KIND of recipe" — and it is the LOAD GATE's own: the
|
|
@@ -815,7 +821,16 @@ function readGradeSeed(seen) {
|
|
|
815
821
|
* spentUsd, spendComplete, carrySpentUsd, carrySpendComplete }`
|
|
816
822
|
*/
|
|
817
823
|
export function readResume(events, { deathAt = null, direct = false, resumableOutcomes = [] } = {}) {
|
|
818
|
-
|
|
824
|
+
let list = Array.isArray(events) ? events.filter(isObj) : [];
|
|
825
|
+
// P5-R: a DIRECT spine may be a resumed run's one file of several legs (src/legs.js). The checkpoint a resume
|
|
826
|
+
// continues from is the latest leg that reached a `job-start`, and that leg's own `job-start` DECLARES the fold
|
|
827
|
+
// it inherited (`priorSpentUsd`/`priorWallMs`/…) — so reading that one window and adding its rounds is the whole
|
|
828
|
+
// chain exactly once, never the earlier legs' rounds on top of a fold that already carries them. A spine with
|
|
829
|
+
// no marker is one leg and reads as it always did.
|
|
830
|
+
if (direct) {
|
|
831
|
+
const legs = legsOf(list);
|
|
832
|
+
if (legs.length > 1) list = (legs.findLast((l) => l.jobStart !== null) ?? legs[legs.length - 1]).records;
|
|
833
|
+
}
|
|
819
834
|
/** which landed terminals are a CHECKPOINT rather than a graded row */
|
|
820
835
|
const resumableHalt = new Set(Array.isArray(resumableOutcomes) ? resumableOutcomes : []);
|
|
821
836
|
// the FIRST reuse-start is the envelope this spine was opened under. A resumed spine
|
|
@@ -1228,7 +1243,9 @@ export const PAUSE_TTL_MS = 60 * 24 * 60 * 60_000;
|
|
|
1228
1243
|
* @returns {{ok: boolean, applies: boolean, ageMs: number|null, ttlMs: number, pausedAt: string|null, detail: string|null}}
|
|
1229
1244
|
*/
|
|
1230
1245
|
export function checkpointAgeGate(events, { now = Date.now, ttlMs = PAUSE_TTL_MS } = {}) {
|
|
1231
|
-
const
|
|
1246
|
+
const all = Array.isArray(events) ? events.filter(isObj) : [];
|
|
1247
|
+
// P5-R: only the run's LATEST leg can be the one waiting — an earlier leg's pause was answered by the leg after it
|
|
1248
|
+
const list = legsOf(all).at(-1)?.records ?? all;
|
|
1232
1249
|
const end = list.filter((e) => e.type === 'job-end').at(-1) ?? null;
|
|
1233
1250
|
const base = { applies: false, ageMs: null, ttlMs, pausedAt: null, detail: null };
|
|
1234
1251
|
if (end?.outcome !== HITL_PAUSE) return { ...base, ok: true };
|
package/src/run.js
CHANGED
|
@@ -256,10 +256,13 @@ async function primitiveSmoke(workdir) {
|
|
|
256
256
|
* passes `false` so the escalation tail's option strings say "resume is
|
|
257
257
|
* `run-u`-only in v1" instead of naming a flag that would fail if typed. Forwarded
|
|
258
258
|
* verbatim to the plan flow, which owns the three `--resume`-naming readouts.
|
|
259
|
+
* @param {string|null} [opts.stopFile=null] P5 item 5 — the run's STOP REQUEST file
|
|
260
|
+
* (`stopFilePath(spine)`, src/legs.js), forwarded verbatim to the plan flow, which reads it at the
|
|
261
|
+
* round-boundary seam (where the money cap binds) and ends the leg `stopped`. `null` = no stop surface.
|
|
259
262
|
* @returns {Promise<string>} outcome: 'green' | 'already-green' | 'escalated' |
|
|
260
263
|
* 'unapproved-spec' | 'job-red' | 'smoke-red' | 'plan-red' | 'check-red' |
|
|
261
264
|
* 'close-red' | 'close-unsupported' | 'recipe-stale' | 'branch-red' | 'pricing-red' | 'provider-red' |
|
|
262
|
-
* 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'step-stalled' |
|
|
265
|
+
* 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'stopped' | 'step-stalled' |
|
|
263
266
|
* 'hitl-pause' | 'hitl-decision-red' | `step-red:<id>`
|
|
264
267
|
* NOTE: `job-end` can ALSO land with outcome 'runner-drained' (F140/PRD item
|
|
265
268
|
* 28(c)) — the `beforeExit` backstop's own terminal, emitted from OUTSIDE this
|
|
@@ -270,7 +273,7 @@ async function primitiveSmoke(workdir) {
|
|
|
270
273
|
* deliberately NOT in the resumable/checkpoint set (`src/reuse.js`) — an
|
|
271
274
|
* unknown in-flight state is not a known-safe resume point.
|
|
272
275
|
*/
|
|
273
|
-
export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider = null, judgeModel = null, rates = null, judgeRates = null, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, closeDir = null, layerRoot = false, readShim = false, scout = true, bridge = null, draftSpentUsd = 0, draftSpendComplete = true, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null, heldRuling = null, reviewDoor = null, doorRerun = null, resumable = true }) {
|
|
276
|
+
export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider = null, judgeModel = null, rates = null, judgeRates = null, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, closeDir = null, layerRoot = false, readShim = false, scout = true, bridge = null, draftSpentUsd = 0, draftSpendComplete = true, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null, heldRuling = null, reviewDoor = null, doorRerun = null, resumable = true, stopFile = null }) {
|
|
274
277
|
// THE READ SHIM's ARM, resolved at the door — the FIRST thing this entry does,
|
|
275
278
|
// before the ledger, before the approval gate, before a byte of the spec is read.
|
|
276
279
|
// An unrecognised spelling throws here at zero cost instead of being coerced by
|
|
@@ -537,7 +540,7 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
537
540
|
// accounts it natively (F12) and the job-end money contract is unchanged.
|
|
538
541
|
{
|
|
539
542
|
const outcome = await runPlan(job, {
|
|
540
|
-
workdir, provider, nativeProvider, providerFor, judgeProvider, judgeModel, rates, judgeRates, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, closeDir, layerRoot, readShim, scout, bridge, priorWallMs: chainWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun, priorSpentUsd: chainFoldUsd, resumable,
|
|
543
|
+
workdir, provider, nativeProvider, providerFor, judgeProvider, judgeModel, rates, judgeRates, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, closeDir, layerRoot, readShim, scout, bridge, priorWallMs: chainWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun, priorSpentUsd: chainFoldUsd, resumable, stopFile,
|
|
541
544
|
// hamr's ruling 2026-09-28 — ONE cap covers drafting + run: the run's
|
|
542
545
|
// enforced ceiling is the signed `budgetUsd` (unchanged, still in the
|
|
543
546
|
// spec hash) MINUS whatever drafting already spent on this job before
|
package/src/runlist.js
CHANGED
|
@@ -58,9 +58,12 @@ export function runlistPath(home) {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
|
-
* @typedef {{ at: string, runid: string, job: string, spine: string, patient: string|null, via: 'run-u'|'bundle'|'backfill', pid?: number, capUsd?: number }} RunRow
|
|
61
|
+
* @typedef {{ at: string, runid: string, job: string, spine: string, patient: string|null, via: 'run-u'|'bundle'|'backfill', pid?: number, capUsd?: number, leg?: number }} RunRow
|
|
62
62
|
* `pid`/`capUsd` (optional; older and backfilled rows carry neither): the runner's process id and the
|
|
63
63
|
* leg's $ cap — what the monthly limit holds while that pid is a live bareloop runner (src/monthly.js).
|
|
64
|
+
* `leg` (P5-R; only on a row {@link readRunList} FOLDED from `leg-start` entries, i.e. a RESUMED run):
|
|
65
|
+
* the number of the run's latest leg — a resumed run is the same run, one row, and its `pid`/`capUsd`
|
|
66
|
+
* are that latest leg's.
|
|
64
67
|
*/
|
|
65
68
|
|
|
66
69
|
/**
|
|
@@ -101,7 +104,10 @@ export function appendRun(row, opts = {}) {
|
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
/**
|
|
104
|
-
* @typedef {{ runid: string, type: 'settled'|'released', by: string, at: string, reason?: string, spentUsd?: number, spendComplete?: boolean }} RunEvent
|
|
107
|
+
* @typedef {{ runid: string, type: 'settled'|'released', by: string, at: string, reason?: string, spentUsd?: number, spendComplete?: boolean, leg?: number }} RunEvent
|
|
108
|
+
* `leg` (P5-R): which leg of the run the write-back is about; absent = leg 1 (every entry written before P5-R).
|
|
109
|
+
* A resumed run is ONE row, so `settled`/`released` are per leg: a later leg's `released` gives back THAT leg's
|
|
110
|
+
* claim and never removes the run.
|
|
105
111
|
* A claim's write-back, appended (never rewritten) beside the run rows. `settled` = the run's claim on the
|
|
106
112
|
* monthly limit is over (`spentUsd`/`spendComplete` are its final figure, `by` is WHO wrote it: the run itself
|
|
107
113
|
* at its `job-end`, or the next run that found its process gone, `reason: 'process gone'`). `released` = the
|
|
@@ -122,6 +128,43 @@ export function appendRunEvent(event, opts = {}) {
|
|
|
122
128
|
try { chmodSync(path, 0o600); } catch { /* best-effort perms */ }
|
|
123
129
|
}
|
|
124
130
|
|
|
131
|
+
/**
|
|
132
|
+
* @typedef {{ type: 'leg-start', runid: string, leg: number, pid?: number, capUsd?: number, at: string }} LegStart
|
|
133
|
+
* A resumed run's NEW claim (P5-R, hamr 2026-10-02: one run, one id, one row): appended beside the run's row
|
|
134
|
+
* instead of a second row. {@link readRunList} folds it into the row.
|
|
135
|
+
*/
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Append one {@link LegStart}: a resumed run's leg claim. Not locked: one short line, one append. THROWS on IO failure.
|
|
139
|
+
* @param {LegStart} entry
|
|
140
|
+
* @param {{ home?: string }} [opts]
|
|
141
|
+
* @returns {void}
|
|
142
|
+
*/
|
|
143
|
+
export function appendLegStart(entry, opts = {}) {
|
|
144
|
+
const path = runlistPath(opts.home);
|
|
145
|
+
mkdirSync(runlistHome(opts.home), { recursive: true, mode: 0o700 });
|
|
146
|
+
appendFileSync(path, `${JSON.stringify({ ...entry, type: 'leg-start' })}\n`);
|
|
147
|
+
try { chmodSync(path, 0o600); } catch { /* best-effort perms */ }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The leg a write-back names (absent = 1).
|
|
152
|
+
* @param {{ leg?: number }} e
|
|
153
|
+
* @returns {number}
|
|
154
|
+
*/
|
|
155
|
+
const legOf = (e) => (Number.isInteger(e.leg) && /** @type {number} */ (e.leg) >= 1 ? /** @type {number} */ (e.leg) : 1);
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Has this row's LATEST leg been settled? The ONE spelling (the monthly limit and the settle writer share it).
|
|
159
|
+
* @param {RunEvent[]} events {@link readRunList}'s `events`
|
|
160
|
+
* @param {{ runid: string, leg?: number }} row
|
|
161
|
+
* @returns {boolean}
|
|
162
|
+
*/
|
|
163
|
+
export function isSettled(events, row) {
|
|
164
|
+
const leg = legOf(row);
|
|
165
|
+
return events.some((e) => e.type === 'settled' && e.runid === row.runid && legOf(e) === leg);
|
|
166
|
+
}
|
|
167
|
+
|
|
125
168
|
/**
|
|
126
169
|
* Tolerant reader — reuses {@link parseJsonl} (never a second hand-rolled
|
|
127
170
|
* parser). An absent file reads as an empty list, not an error: a fresh
|
|
@@ -136,20 +179,45 @@ export function readRunList(opts = {}) {
|
|
|
136
179
|
const path = runlistPath(opts.home);
|
|
137
180
|
if (!existsSync(path)) return { rows: [], skipped: 0, events: [] };
|
|
138
181
|
const { records, skipped } = parseJsonl(path);
|
|
139
|
-
// the FIRST settle for a runid is authoritative: two runs may both close the same dead claim, and the
|
|
182
|
+
// the FIRST settle for a (runid, leg) is authoritative: two runs may both close the same dead claim, and the
|
|
140
183
|
// second note is a duplicate (a `released` line likewise); file order decides, later copies are ignored
|
|
141
184
|
const seen = new Set();
|
|
142
185
|
/** @type {RunEvent[]} */
|
|
143
186
|
const events = records.filter((r) => {
|
|
144
187
|
if (!r || (r.type !== 'settled' && r.type !== 'released')) return false;
|
|
145
|
-
const key = `${r.type}:${r.runid}`;
|
|
188
|
+
const key = `${r.type}:${r.runid}:${legOf(r)}`;
|
|
146
189
|
if (seen.has(key)) return false;
|
|
147
190
|
seen.add(key);
|
|
148
191
|
return true;
|
|
149
192
|
});
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
193
|
+
// a leg-1 `released` = the run never started: not listed. A LATER leg's `released` only gives that leg's claim back.
|
|
194
|
+
const released = new Set(events.filter((e) => e.type === 'released' && legOf(e) === 1).map((e) => e.runid));
|
|
195
|
+
const voided = new Set(events.filter((e) => e.type === 'released' && legOf(e) > 1).map((e) => `${e.runid}:${legOf(e)}`));
|
|
196
|
+
/** @type {Map<string, RunRow & {claimAt: number}>} */
|
|
197
|
+
const byRunid = new Map();
|
|
198
|
+
/** @type {(RunRow & {claimAt: number})[]} */
|
|
199
|
+
const rows = [];
|
|
200
|
+
records.forEach((r, i) => {
|
|
201
|
+
if (!r) return;
|
|
202
|
+
if (!r.type) {
|
|
203
|
+
if (released.has(r.runid)) return;
|
|
204
|
+
const row = { ...r, claimAt: i };
|
|
205
|
+
rows.push(row);
|
|
206
|
+
if (!byRunid.has(r.runid)) byRunid.set(r.runid, row);
|
|
207
|
+
} else if (r.type === 'leg-start') {
|
|
208
|
+
const row = byRunid.get(r.runid);
|
|
209
|
+
if (!row || !Number.isInteger(r.leg) || r.leg < 2 || voided.has(`${r.runid}:${r.leg}`) || r.leg <= legOf(row)) return;
|
|
210
|
+
// the run's pid/cap are its LATEST leg's; the claim's place in the file (the monthly limit's "who is above
|
|
211
|
+
// whom") is that leg-start line's, since that is where the leg claimed
|
|
212
|
+
row.leg = r.leg;
|
|
213
|
+
row.pid = r.pid;
|
|
214
|
+
row.capUsd = r.capUsd;
|
|
215
|
+
row.claimAt = i;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
// file order = claim order: a resumed run's latest claim sits where its latest leg-start line is
|
|
219
|
+
const ordered = rows.some((r) => r.leg !== undefined) ? [...rows].sort((a, b) => a.claimAt - b.claimAt) : rows;
|
|
220
|
+
return { rows: ordered.map(({ claimAt: _c, ...row }) => row), skipped, events };
|
|
153
221
|
}
|
|
154
222
|
|
|
155
223
|
/**
|
|
@@ -213,6 +281,21 @@ function runidForSpine(spinePath) {
|
|
|
213
281
|
// the panel) because the monthly limit reads the same rule (src/monthly.js).
|
|
214
282
|
export const DIED_MTIME_MS = 10 * 60 * 1000;
|
|
215
283
|
|
|
284
|
+
/**
|
|
285
|
+
* The ONE "is this run alive" rule, for the panel's glyph ([▶] vs [?]) and the monthly limit's hold. A
|
|
286
|
+
* row that carries its runner's `pid` is alive exactly while that pid is a live bareloop runner
|
|
287
|
+
* ({@link isLiveRunner}) however quiet its spine; only a row with NO pid (an older row) falls back to
|
|
288
|
+
* the spine-mtime rule above. A run that has ENDED (a `job-end` on its spine) is a different fact the
|
|
289
|
+
* callers read off the spine itself — this answers "is a process still running it", nothing more.
|
|
290
|
+
* @param {{ pid?: number, spine: string }} row
|
|
291
|
+
* @param {number} [nowMs] the clock for the mtime fallback
|
|
292
|
+
* @returns {boolean}
|
|
293
|
+
*/
|
|
294
|
+
export function runIsAlive(row, nowMs = Date.now()) {
|
|
295
|
+
if (Number.isInteger(row.pid)) return isLiveRunner(/** @type {number} */ (row.pid));
|
|
296
|
+
try { return nowMs - statSync(row.spine).mtimeMs <= DIED_MTIME_MS; } catch { return false; }
|
|
297
|
+
}
|
|
298
|
+
|
|
216
299
|
// A backfill scan of a real patients directory can be arbitrarily deep
|
|
217
300
|
// (person-path runs archive several levels down: `<dir>/<proj>/out/
|
|
218
301
|
// source-<x>/<proj>-bareloop/u-<id>.jsonl`, 4 levels under the dir a person
|
package/src/text.js
CHANGED
|
@@ -308,3 +308,23 @@ export function stampRaw(raw, { cause = null, reason = null }) {
|
|
|
308
308
|
raw.reason = reason === null || reason === undefined ? null : redactSecrets(reason);
|
|
309
309
|
return raw;
|
|
310
310
|
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* BA-21 — the WRITE side of the pricing-provenance signal (read side: `rateProvenance` /
|
|
314
|
+
* `spendProvenance` in src/ledger.js). bare-agent >=0.37 rides `rateSource` beside
|
|
315
|
+
* `pricing` on every metering payload; both metering callbacks below forward it onto the
|
|
316
|
+
* spine through THIS one helper, because two sites spelling one rule are two instruments
|
|
317
|
+
* (the ripgrep fix that landed in ci.yml but not publish.yml).
|
|
318
|
+
*
|
|
319
|
+
* Forwarded VERBATIM — the write side reports what upstream said, and src/ledger.js is
|
|
320
|
+
* the one place that decides what counts as a guess. And an ABSENT provenance stays
|
|
321
|
+
* absent: a payload carrying no `rateSource` (bare-agent <0.37, and every round already
|
|
322
|
+
* in the archive) emits no field at all, so a reader sees UNKNOWN rather than a label we
|
|
323
|
+
* invented. Never defaulted to `null` here — `null` is upstream saying "nothing was
|
|
324
|
+
* priced", which is a different fact from "nobody told us".
|
|
325
|
+
* @param {any} arg an `onLlmResult` / `onTurn` payload
|
|
326
|
+
* @returns {{rateSource?: string|null}} the field to spread into the spine record
|
|
327
|
+
*/
|
|
328
|
+
export function rateSourceFields(arg) {
|
|
329
|
+
return arg && typeof arg === 'object' && 'rateSource' in arg ? { rateSource: arg.rateSource } : {};
|
|
330
|
+
}
|
package/src/u-readout.js
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
// leads; the leg is what this process actually bought, and dropping it would trade
|
|
17
17
|
// one blindness for the other.
|
|
18
18
|
|
|
19
|
+
import { legsOf } from './legs.js';
|
|
20
|
+
|
|
19
21
|
/** minutes, at the precision the readout has ever claimed (~90%, hamr's ruling) */
|
|
20
22
|
const min = (/** @type {number} */ ms) => (ms / 60_000).toFixed(1);
|
|
21
23
|
|
|
@@ -336,10 +338,16 @@ export function doorLines({ rerun, accept, pause }) {
|
|
|
336
338
|
*/
|
|
337
339
|
export function deathAtOf({ watchdogAt, events } = {}) {
|
|
338
340
|
const list = Array.isArray(events) ? events : [];
|
|
341
|
+
// P5-R: a resumed run is one spine of several legs. "Did this run end itself" is a question about its
|
|
342
|
+
// LATEST leg: an earlier leg's job-end says nothing about how the leg now being read stopped, and a
|
|
343
|
+
// watchdog note dated before this leg began is a different leg's note.
|
|
344
|
+
const last = legsOf(list).at(-1) ?? null;
|
|
345
|
+
const scope = last ? last.records : list;
|
|
339
346
|
// a landed terminal is the run dating its own stop — nothing outside it is better
|
|
340
347
|
// evidence about a run that was never killed
|
|
341
|
-
if (
|
|
348
|
+
if (scope.some((e) => e !== null && typeof e === 'object' && /** @type {any} */ (e).type === 'job-end')) return null;
|
|
342
349
|
const at = typeof watchdogAt === 'string' ? Date.parse(watchdogAt) : NaN;
|
|
350
|
+
if (last && last.leg > 1 && last.startMs !== null && Number.isFinite(at) && at < last.startMs) return null;
|
|
343
351
|
// an unreadable stamp is UNKNOWN, never a NaN handed to a fold (F6): the caller's
|
|
344
352
|
// own default is the honest fallback
|
|
345
353
|
return Number.isFinite(at) ? at : null;
|
package/src/u-watchdog.mjs
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
//
|
|
44
44
|
// Usage:
|
|
45
45
|
// node src/u-watchdog.mjs --spine <path> --pid <n>
|
|
46
|
-
// [--stale-ms N] [--wall-ms N] [--grace-ms N] [--dead-ms N] [--poll-ms N] [--term-grace-ms N]
|
|
46
|
+
// [--stale-ms N] [--wall-ms N] [--grace-ms N] [--dead-ms N] [--poll-ms N] [--term-grace-ms N] [--report <path>]
|
|
47
47
|
import { statSync, writeFileSync, renameSync, unlinkSync } from 'node:fs';
|
|
48
48
|
|
|
49
49
|
const arg = (/** @type {string} */ n, /** @type {string|null} */ dflt = null) => {
|
|
@@ -215,7 +215,9 @@ function stop(/** @type {string} */ reason, /** @type {string} */ judgement, /**
|
|
|
215
215
|
// not a guard (F70: the instrument carrying the failure mode it exists to catch). The
|
|
216
216
|
// kill below runs whether or not this succeeded; a failure is named on stderr so a
|
|
217
217
|
// missing marker is never read as "the guard never fired".
|
|
218
|
-
|
|
218
|
+
// a RESUMED leg writes its own note beside the run's one spine (P5-R: `<spine>.leg<N>.watchdog.json`), so a
|
|
219
|
+
// later leg never overwrites the earlier leg's evidence; the default is the file every run has always had
|
|
220
|
+
const reportPath = arg('report') ?? `${spine}.watchdog.json`;
|
|
219
221
|
const tmpPath = `${reportPath}.tmp`;
|
|
220
222
|
try {
|
|
221
223
|
writeFileSync(tmpPath, `${JSON.stringify(record, null, 2)}\n`);
|