bareloop 0.33.0 → 0.34.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 +16 -0
- package/bareloop.context.md +22 -14
- package/package.json +1 -1
- package/src/interviewrun.js +3 -2
- package/src/job.js +2 -2
- package/src/judged.js +24 -4
- package/src/kinds.js +1 -0
- package/src/ledger.js +3 -4
- package/src/monthly.js +28 -21
- package/src/panel/authorroutes.js +12 -2
- package/src/panel/server.js +18 -13
- package/src/planrun.js +3 -5
- package/src/providerrows.js +4 -2
- package/src/providers.js +4 -4
- package/src/runlist.js +15 -0
- package/src/userrun.js +31 -11
- package/types/judged.d.ts +5 -2
- package/types/monthly.d.ts +17 -5
- package/types/panel/authorroutes.d.ts +15 -0
- package/types/providerrows.d.ts +2 -1
- package/types/providers.d.ts +1 -2
- package/types/runlist.d.ts +14 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ All notable changes to bareloop are documented here. Format:
|
|
|
5
5
|
[SemVer](https://semver.org/spec/v2.0.0.html). Pre-1.0: **minor** = a ladder rung or
|
|
6
6
|
feature lands, **patch** = docs, fixes, scaffolding.
|
|
7
7
|
|
|
8
|
+
## [0.34.0] - 2026-10-01
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Judge rounds now carry `rateSource` (`'caller'` under your price, else the guess label the worker rounds carry), so a customer-priced judge round counts as vouched, not other, in the provenance readout and the monthly counts. Dollar totals are unchanged. Evidence: live run `mup3h70u` (136 worker rounds `caller`, 14 judge rounds without it).
|
|
13
|
+
- The monthly-limit refusal now names the money a run in progress holds: `Max $4.00 (monthly limit, $5.95 held by a run in progress)` (`runs` when several); with nothing held the text is unchanged. `monthSpend` / `checkMonthlyRoom` also return `heldUsd` and `heldRuns`.
|
|
14
|
+
- The interview's "Run it now?" now starts the packaged door (`bin/bareloop.mjs author`, same flags) instead of `scripts/run-author.mjs`, which the published package does not ship.
|
|
15
|
+
- One "is this run alive" rule (`runIsAlive`, `src/runlist.js`) now drives both the panel's [▶] / [?] glyph and the monthly limit's hold: the run row's pid first (`isLiveRunner`), the spine-mtime rule only for a row with no pid. A run whose pid is alive but whose spine is quiet for 10+ minutes now reads running, not died; a dead pid with a fresh spine reads died. A `job-end` on the spine still wins.
|
|
16
|
+
- `--resume` now also refuses when the predecessor run's own run-list row carries the pid of a live bareloop runner, not only when a watchdog kill record names it (same refusal text).
|
|
17
|
+
- Every panel GET/HEAD route now refuses (`403`) a request whose `Host` is not the panel's own `127.0.0.1:<port>` (one shared `checkHostGuard`, also used by the write routes' guard), so a page on another origin cannot read the run list through a rebound DNS name. No token is needed to read.
|
|
18
|
+
- The monthly-limit check (and so every run start) no longer parses the spine of every run ever listed: it reads only runs listed in the current local month, plus any claim still held by a live runner. The Money tab's all-time figures still read everything.
|
|
19
|
+
- A key row priced 0 / 0 is still accepted, but the `price yours:` readout (run preview and run tail) now ends with a warning that the price is $0 so spend and limits read $0.
|
|
20
|
+
- A run that stops at $0 on a SOURCE-MANIFEST-RED, DESTINATION-RED or CLOSE-TIMING-RED now releases its monthly claim (the same `released` entry as a run that never started), so it leaves no ghost row and the month no longer reads "at least".
|
|
21
|
+
- A spec whose `budgetUsd` is not a finite number (e.g. `1e999`) is refused by `validateJob` at $0, and the monthly-limit check (`checkMonthlyRoom` / `claimRun`) no longer reads a non-finite cap as $0 — it never fits.
|
|
22
|
+
- A bundle money halt that used the whole signed ceiling now says no room is left under the signed ceiling (and prints no resume line) instead of a `--budget <more than X, at most X>` placeholder nothing can satisfy.
|
|
23
|
+
|
|
8
24
|
## [0.33.0] — 2026-09-30
|
|
9
25
|
|
|
10
26
|
### Added
|
package/bareloop.context.md
CHANGED
|
@@ -158,7 +158,7 @@ minting claim, or the shell-owned retry cap — all unknown-field reds.
|
|
|
158
158
|
| `provider` | `anthropic-api` \| `openai-api` \| `gemini-api` \| `clipipe-subscription` | menu (`PROVIDERS`); part of the lineage key by definition. Only `anthropic-api` is guaranteed (F48); `openai-api` (PRD item 28) goes through the provider factory (`src/providers.js`), reads `OPENAI_API_KEY`, and admits only `deepseek-flash` today (F171: swapped from the retired `deepseek-chat`) — see "Worker surface" above for its `baseUrl` field and per-model gating; `gemini-api` (PRD item 31.3) reads `GEMINI_API_KEY` and is ADMITTED-PENDING-PROBE — zero runs, and every launch says so (`PROBE_STATUS`/`probeWarningLines`); `clipipe-subscription` drives the worker natively and needs `opts.nativeProvider` |
|
|
159
159
|
| `conditions` | `{ providerPath?, closeVerbosity?, taskFraming?, scaffold? }` | declared keys only, string values — the environment label (consumed by the N3 lineage key; recorded on spines from run one) |
|
|
160
160
|
| `cadence` | `{ unit: hour\|day\|week, every: 1..30 }` | validated now, consumed at N5 (Scheduler) |
|
|
161
|
-
| `budgetUsd` | `0 < n <= shell cap` | ceiling chain: workflow ≤ job ≤ shell — each layer may tighten, never exceed |
|
|
161
|
+
| `budgetUsd` | finite `0 < n <= shell cap` | ceiling chain: workflow ≤ job ≤ shell — each layer may tighten, never exceed |
|
|
162
162
|
| `maxWallMs` | optional integer ms `>= MIN_WALL_MS` (one close timeout) | the run's wall clock. **NO DEFAULT, by ruling** — absent means time-unbounded *by explicit operator choice*, never by fallback (F45: a defaulted cap is a silent second ceiling). Enforcement is a BETWEEN-ROUND deadline, so the honest worst case is `maxWallMs + closeStages × closeTimeoutMs` — every stage of a staged close gets the FULL timeout — and all three numbers are reported (`loop.stop()` cannot cut an in-flight call — F61 measured 500ms→4,018ms). The `MIN_WALL_MS` floor is a ONE-stage, un-autoset-default number, so a spec with many stages OR a raised/autoset `closeTimeoutMs` can validate while its overshoot dwarfs its cap: `runPlan` adds a RUN-START check (PRD item 27/M3) that refuses `wall-under-close-timeout` at $0 once the EFFECTIVE per-stage ceiling is known (autoset or signed), rather than a silent clamp. Operator-only, tighten-only; adding or changing it changes the spec hash |
|
|
163
163
|
| `closeTimeoutMs` | optional integer ms `>= CLOSE_TIMEOUT_FLOOR_MS` (120,000 — hamr's arbiter floor, 2026-09-07) | PRD item 27/M3's SIGNED override for the close's own per-stage wall-clock cap. Absent (the normal case): the run autosets it from a $0 seed timing pass — `max(FLOOR, K × slowest measured stage)`, `K = 5` (hamr, 2026-09-07: "2 and 5 are fine"). Present: the operator's own number wins outright and may legally sit ABOVE *or below* the autoset estimate (the rates-passthrough shape, F113 — never tighten-only). Arbiter territory exactly like `budgetUsd`/`maxWallMs`/`close[].sha256`: the authoring pipeline's `assembleSpec` refuses a draft carrying it |
|
|
164
164
|
| `model` | optional non-empty string, exact provider model id (e.g. `"claude-sonnet-5"`) | the WORKER's model, build-list #3. Absent means today's runner default; present it wins over a `--model` flag outright, and a flag naming a different id is refused (`resolveWorkerModel`, `src/job.js`) rather than silently overridden. Part of the signed hash like every other field. Worker only — the JUDGE's model comes from the `judge` row below, which DEFAULTS to this same worker model |
|
|
@@ -1860,10 +1860,10 @@ before. bareloop keeps no price list of its own, ever.
|
|
|
1860
1860
|
carries it on every metering payload, and bareloop forwards it VERBATIM (`rateSourceFields`,
|
|
1861
1861
|
`src/planrun.js`): every API `worker-round` (worker, scout, planner, fix loop) carries it beside
|
|
1862
1862
|
`pricing` today — `'caller'` when a customer price is on the key's row, else `'tier'`/`'default'`
|
|
1863
|
-
(the guess).
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1863
|
+
(the guess). `judge-round` records carry it too (the judge Loop's per-call label, forwarded by
|
|
1864
|
+
`src/kinds.js` `onJudgeCost`): `'caller'` under your price on the judge's row, else the guess label. The
|
|
1865
|
+
exceptions read UNKNOWN provenance, correctly, and by the same rule that governs the archive: every
|
|
1866
|
+
round archived before the signal existed (judge rounds before this change included). The native
|
|
1867
1867
|
per-turn `worker-turn` carries `null`, bareloop's OWN statement rather than a forwarded one: that
|
|
1868
1868
|
surface prices the SESSION, so a turn had no rate to guess. A `worker-round`/`worker-turn`
|
|
1869
1869
|
carries bare-agent's own label beside `pricing`:
|
|
@@ -3165,6 +3165,9 @@ monthly limit, your price and the key-row choice apply exactly as on the sub-com
|
|
|
3165
3165
|
got none, and a run started from the menu skipped the limit). All three are dispatched by name only: `bareloop run-u <flags…>`,
|
|
3166
3166
|
`bareloop interview <flags…>`, `bareloop author <flags…>`.
|
|
3167
3167
|
|
|
3168
|
+
When two key rows share one provider and endpoint, a CLI run uses the FIRST row in the keys list, so put the
|
|
3169
|
+
key you want first (the panel picks the row by the Model menu).
|
|
3170
|
+
|
|
3168
3171
|
- **`bareloop export <jobs/x.json> --registry <dir> --out <dir>`** → resolves the spec's
|
|
3169
3172
|
close-script paths against the spec file's own directory (never the process cwd), calls
|
|
3170
3173
|
`exportBundle`. On success prints `bundleHash:`, the sorted file list, and "this bundle is
|
|
@@ -3205,7 +3208,8 @@ got none, and a run started from the menu skipped the limit). All three are disp
|
|
|
3205
3208
|
`--budget`/`--wall`, only if you passed them (a tightened ceiling never silently reverts to the signed
|
|
3206
3209
|
one). After a MONEY halt the line does NOT repeat the exhausted `--budget` (chain spend is folded, so
|
|
3207
3210
|
it would halt again at once); it prints `--budget <more than $<spent so far>, at most $<signed
|
|
3208
|
-
budgetUsd>>` for you to fill in, and is not runnable until you do.
|
|
3211
|
+
budgetUsd>>` for you to fill in, and is not runnable until you do. When the run spent the WHOLE signed
|
|
3212
|
+
ceiling, that range is empty, so no resume line is printed: the block says no room is left under the signed ceiling. A bundle's spec is
|
|
3209
3213
|
manifest-hashed, so its halt hint never asks you to edit `budgetUsd`.
|
|
3210
3214
|
6. `resolveBundleSpec` (the `$BARELOOP_BUNDLE` substitution) → tighten `budgetUsd`/`maxWallMs`
|
|
3211
3215
|
in memory if `--budget`/`--wall` were given → `approveHash = jobSpecHash(that tightened+
|
|
@@ -3325,6 +3329,8 @@ got none, and a run started from the menu skipped the limit). All three are disp
|
|
|
3325
3329
|
`baselineKind`/`question`: the plain-English question is derived in `src/panel/server.js`
|
|
3326
3330
|
(`stageQuestionText`) from the signed stage's kind/params only, `null` when no honest
|
|
3327
3331
|
wording exists (the client then shows the stage's own name). Every
|
|
3332
|
+
GET/HEAD answers only when its `Host` header is the panel's own `127.0.0.1:<port>` (`checkHostGuard`,
|
|
3333
|
+
`src/panel/authorroutes.js`; any other Host is `403`, no token needed to read). Every
|
|
3328
3334
|
P1 endpoint is GET/HEAD only (anything else — including every write verb — is `405`, outside the
|
|
3329
3335
|
two human-click-guarded write families `/api/author/*` and `/api/settings/*` below); a URL
|
|
3330
3336
|
never joins a path segment into a filesystem read — a runid is looked up in the run list
|
|
@@ -3370,7 +3376,8 @@ got none, and a run started from the menu skipped the limit). All three are disp
|
|
|
3370
3376
|
requires the human-click guard: an `x-bareloop-token` header matching a fresh token minted
|
|
3371
3377
|
once per server start (templated into `index.html` like the port) AND an Origin/Host
|
|
3372
3378
|
naming this exact `127.0.0.1:<port>` — a request failing either gets `403`, before the
|
|
3373
|
-
route body ever runs.
|
|
3379
|
+
route body ever runs. The token protects against other browser origins, not against other local users or
|
|
3380
|
+
processes on the same machine (local-trust model). `POST /api/author/start` (one job-card body; `409` while a prior
|
|
3374
3381
|
session is still non-terminal — one authoring session at a time) creates a session
|
|
3375
3382
|
(`src/panel/authorsession.js`) that runs `prepareSource`/`detectLanguage`/`validateJob`/
|
|
3376
3383
|
`authorCloseForJob`/`assembleSpec`/`prepareSigning` in-process, exactly the library calls
|
|
@@ -3440,7 +3447,7 @@ got none, and a run started from the menu skipped the limit). All three are disp
|
|
|
3440
3447
|
mechanically, $0, via `prepareSource`/`proveDestination`), then the picked class's frozen
|
|
3441
3448
|
question set (`questionsFor`/`requiredAnswersFor`, `src/authorjob.js`), then the operator's
|
|
3442
3449
|
own job name/budget/wall. On success it writes `answers.json`/`specdraft.json` under `--out`
|
|
3443
|
-
and either offers to spawn `scripts/run-author.mjs
|
|
3450
|
+
and either offers to spawn the packaged `bin/bareloop.mjs author` (same flags as `scripts/run-author.mjs`, which the package does not ship; only when a clean key is present and no
|
|
3444
3451
|
install gap remains — default answer is no) or prints the exact command to run it later.
|
|
3445
3452
|
Because this module reads a real TTY/piped stdin, `src/cli.js` hands it `stdin`/`stdout`/
|
|
3446
3453
|
`stderr` (the raw streams, not the wrapped `out`/`err` line-functions every other command
|
|
@@ -3559,7 +3566,7 @@ provider its own row, for the worker's provider the row on the worker's endpoint
|
|
|
3559
3566
|
(The judge's KEY still follows the worker's row, so with two rows on one endpoint key and price can differ.) A priced round is stamped `rateSource:'caller'` (VOUCHED, see `spendProvenance`); with
|
|
3560
3567
|
no price it stays the built-in guess, as before. The run preview and the run tail print
|
|
3561
3568
|
`price yours: in $0.006 / out $1.20 per 1M tokens (DEEPSEEK_API_KEY row)` when a price is set, and
|
|
3562
|
-
nothing when none is
|
|
3569
|
+
nothing when none is; a 0 / 0 price is accepted but the line then ends `— WARNING: this price is $0, so spend and limits read $0`. **The price is yours, and it sets what the cap means:** the cap and the halts run
|
|
3563
3570
|
on the dollars your price produces, so a price set too low weakens the cap (a run can do far more
|
|
3564
3571
|
than you meant before it halts). When a vendor lists two prices (for example a cache-hit and a
|
|
3565
3572
|
cache-miss input rate) enter the higher one. bare-agent applies its own cache multipliers (read 0.1×,
|
|
@@ -3570,8 +3577,9 @@ re-priced** (a spine and the run list keep the dollars they booked). Not covered
|
|
|
3570
3577
|
resolves no key row.
|
|
3571
3578
|
|
|
3572
3579
|
**Monthly $ limit.** `checkMonthlyRoom({ capUsd, home, now })` → `{ ok, leftUsd, limitUsd,
|
|
3573
|
-
atLeast }`; `monthlyRefusalText(room)` is the ONE spelling: `Max $<left, 2 decimals>
|
|
3574
|
-
limit)
|
|
3580
|
+
atLeast, heldUsd, heldRuns }`; `monthlyRefusalText(room)` is the ONE spelling: `Max $<left, 2 decimals>
|
|
3581
|
+
(monthly limit)`, and when run(s) in progress hold money beyond what they have spent, the same line
|
|
3582
|
+
reads `Max $4.00 (monthly limit, $5.95 held by a run in progress)` (`runs` when more than one). It is called at the run-start seam in `src/userrun.js` `execute` (before any token
|
|
3575
3583
|
spends; the CLI prints it to stderr, exit 2) and at the panel's Sign & run (`signRun` in
|
|
3576
3584
|
`src/panel/authorroutes.js`, refused server-side whatever the page shows). The page only echoes the
|
|
3577
3585
|
same text under `#jf-cap-money` via `GET /api/author/monthly-check?cap=`. "This month" is the local
|
|
@@ -3584,17 +3592,17 @@ records only. `claimRun` (`src/monthly.js`) writes the run's row FIRST (it carri
|
|
|
3584
3592
|
its pid is a live bareloop runner (`isLiveRunner`, `src/runlist.js`, also what `--resume` uses to refuse a
|
|
3585
3593
|
resume against a live run: the process is a node executable with a `bareloop`/`bareloop.mjs`/`run-u.mjs`/
|
|
3586
3594
|
`u-watchdog.mjs` argument, or is itself named so — a recycled pid running some other program never holds a
|
|
3587
|
-
cap, though a recycled node program with such an argument still would), however quiet its spine; a claim whose process is gone is closed by whoever
|
|
3595
|
+
cap, though a recycled node program with such an argument still would), however quiet its spine (`runIsAlive`, `src/runlist.js`, is the one "is this run alive" rule: the panel's [▶] / [?] glyph reads it too; `--resume` also refuses on the pid in the predecessor's own run-list row, not only the watchdog's record); a claim whose process is gone is closed by whoever
|
|
3588
3596
|
finds it, with a note (`{runid, type:'settled', by:<finder>, reason:'process gone', spentUsd:<floor>}`) and
|
|
3589
3597
|
counts its real spend. If the sum fits, the run goes; if not, it appends `{type:'released',
|
|
3590
3598
|
reason:'refused'}` and refuses with the exact text, nothing spent. A run whose row is listed (claimed, or with no limit set) that then exits at $0 before its spine has a first record (a refused
|
|
3591
|
-
`--resume` patient, a failing tree setup) appends `{type:'released', reason:'not started'}` too, so it leaves no row. A row that cannot be written, or an
|
|
3599
|
+
`--resume` patient, a failing tree setup) appends `{type:'released', reason:'not started'}` too, so it leaves no row; so does a $0 refusal after the spine exists (`SOURCE-MANIFEST-RED`, `DESTINATION-RED`, `CLOSE-TIMING-RED`). A row that cannot be written, or an
|
|
3592
3600
|
unreadable config, refuses too — never "no limit". When a run ends it appends its own
|
|
3593
3601
|
`{runid, type:'settled', by:<runid>, spentUsd, spendComplete, at}`. Two runs may close the same dead claim;
|
|
3594
3602
|
the FIRST note for a runid is authoritative. `readRunList` folds these entries out of the run rows
|
|
3595
3603
|
(`events`; a `released` claim's row is not listed). Month totals still read real spend off the spines. An
|
|
3596
3604
|
older row with no `pid` keeps the spine-mtime rule (`DIED_MTIME_MS`: a spine written to within it is in
|
|
3597
|
-
flight, at its leg cap). A run refused because an earlier claim was itself later refused is the accepted
|
|
3605
|
+
flight, at its leg cap). The refusal check parses only the spines of runs listed in the current local month (plus any claim still held by a live runner); the all-time readers still read every run. A run refused because an earlier claim was itself later refused is the accepted
|
|
3598
3606
|
safe-side cost of doing without a lock.
|
|
3599
3607
|
Only the refusal reserves the cap (`monthSpend().reservedUsd`); the Money tab's month figure is
|
|
3600
3608
|
real spend.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bareloop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"description": "Workflows that earn their own design, with receipts — agent-authored workflows for repeated, long, verifiable jobs that improve across runs under an un-gameable gate.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
package/src/interviewrun.js
CHANGED
|
@@ -722,7 +722,8 @@ export async function main(argv, deps = {}) {
|
|
|
722
722
|
}
|
|
723
723
|
|
|
724
724
|
// ── 6. the paid step, which is a DIFFERENT process under a DIFFERENT ceiling ─
|
|
725
|
-
|
|
725
|
+
// the PACKAGED door (`bin/` ships; `scripts/` does not): `bareloop author` takes the same flags as run-author
|
|
726
|
+
const BARELOOP_BIN = fileURLToPath(new URL('../bin/bareloop.mjs', import.meta.url));
|
|
726
727
|
const childArgs = [
|
|
727
728
|
'--source', TREE, '--answers', answersFile, '--draft', draftFile,
|
|
728
729
|
'--verdict', VERDICT, '--out', OUT,
|
|
@@ -832,7 +833,7 @@ export async function main(argv, deps = {}) {
|
|
|
832
833
|
exitCode = 0;
|
|
833
834
|
} else {
|
|
834
835
|
say('');
|
|
835
|
-
const child = spawnSyncFn(process.execPath, [
|
|
836
|
+
const child = spawnSyncFn(process.execPath, [BARELOOP_BIN, 'author', ...childArgs], { stdio: 'inherit' });
|
|
836
837
|
if (child.error) {
|
|
837
838
|
say(`run-author could not be started (${child.error.message}) — the command above still stands.`);
|
|
838
839
|
exitCode = 2;
|
package/src/job.js
CHANGED
|
@@ -289,8 +289,8 @@ export function validateJob(input, { shellCapUsd = 2 } = {}) {
|
|
|
289
289
|
}
|
|
290
290
|
|
|
291
291
|
if (spec.budgetUsd === undefined) red('missing-required', 'budgetUsd');
|
|
292
|
-
else if (!(typeof spec.budgetUsd === 'number' && spec.budgetUsd > 0 && spec.budgetUsd <= shellCapUsd)) {
|
|
293
|
-
red('bounds', 'budgetUsd', `0 < budget <= shell cap ${shellCapUsd} (cap-not-estimate; no self-adjusted budgets, ever)`);
|
|
292
|
+
else if (!(typeof spec.budgetUsd === 'number' && Number.isFinite(spec.budgetUsd) && spec.budgetUsd > 0 && spec.budgetUsd <= shellCapUsd)) {
|
|
293
|
+
red('bounds', 'budgetUsd', `a finite 0 < budget <= shell cap ${shellCapUsd} (cap-not-estimate; no self-adjusted budgets, ever)`);
|
|
294
294
|
}
|
|
295
295
|
|
|
296
296
|
// T (PRD v1.27/v1.29) — TIME, with exactly budgetUsd's status: operator input,
|
package/src/judged.js
CHANGED
|
@@ -793,10 +793,28 @@ function topLevelFunctionNames(artifactText) {
|
|
|
793
793
|
* so the priced seam is the only legal one.
|
|
794
794
|
* `rates` is the customer's own price for the judge's row (USD per 1K, `ratesFor`'s
|
|
795
795
|
* `rates`); null/absent = bare-agent's built-in guess, exactly as before.
|
|
796
|
-
* @param {{provider: any, system: string, rates?: {in: number, out: number}|null}} o
|
|
796
|
+
* @param {{provider: any, system: string, rates?: {in: number, out: number}|null}} o
|
|
797
|
+
* @returns {{run: (msgs: any[], tools: any[], opts?: any) => Promise<any>}} */
|
|
797
798
|
export const defaultJudgeLoop = ({ provider, system, rates = null }) => {
|
|
798
799
|
const { Loop } = require('bare-agent');
|
|
799
|
-
|
|
800
|
+
// BA-21: the Loop's run RESULT carries no `rateSource` — only the per-call metering event
|
|
801
|
+
// does (`onLlmResult`). Capture it there and ride it on the result, so `runLocate` can
|
|
802
|
+
// forward it to the judge-round record beside `costUsd`. Absent (no round metered) = no key.
|
|
803
|
+
/** @type {{rateSource: string|null}|null} */
|
|
804
|
+
let seen = null;
|
|
805
|
+
const loop = new Loop({
|
|
806
|
+
provider, system, ...(rates ? { rates } : {}),
|
|
807
|
+
onLlmResult: (/** @type {{rateSource?: string|null}} */ m) => {
|
|
808
|
+
if (m && typeof m === 'object' && 'rateSource' in m) seen = { rateSource: m.rateSource ?? null };
|
|
809
|
+
},
|
|
810
|
+
});
|
|
811
|
+
return {
|
|
812
|
+
run: async (/** @type {any[]} */ msgs, /** @type {any[]} */ tools, /** @type {any} */ opts) => {
|
|
813
|
+
seen = null;
|
|
814
|
+
const res = await loop.run(msgs, tools, opts);
|
|
815
|
+
return res && typeof res === 'object' && seen ? { ...res, rateSource: /** @type {any} */ (seen).rateSource } : res;
|
|
816
|
+
},
|
|
817
|
+
};
|
|
800
818
|
};
|
|
801
819
|
|
|
802
820
|
// ── LOCATE: one attempt ─────────────────────────────────────────────────────
|
|
@@ -821,7 +839,7 @@ export const defaultJudgeLoop = ({ provider, system, rates = null }) => {
|
|
|
821
839
|
* On every route out, red or clean, `onCost` fires exactly once with the honest
|
|
822
840
|
* read: a paid call that leaves no meter record is F12 wearing a judge's coat.
|
|
823
841
|
* @param {{artifactText: string, card: any, loopFactory: (o: {provider?: any, system: string}) => any,
|
|
824
|
-
* maxTokens?: number, attempt?: number, onCost?: (c: {costUsd: number|null, unpricedRounds: number}) => void,
|
|
842
|
+
* maxTokens?: number, attempt?: number, onCost?: (c: {costUsd: number|null, unpricedRounds: number, rateSource?: string|null}) => void,
|
|
825
843
|
* callBounds?: {timeoutMs?: number, deadlineMs?: number}}} o
|
|
826
844
|
* @returns {Promise<{ok: boolean, facts: any|null, red: {axis: string, detail: string}|null,
|
|
827
845
|
* costUsd: number|null, unpricedRounds: number, truncated: boolean, parseError: boolean,
|
|
@@ -852,7 +870,9 @@ export async function runLocate({
|
|
|
852
870
|
/** @param {{axis: string, detail: string}|null} red @param {any} facts @param {any} r @param {boolean} truncated @param {boolean} parseError */
|
|
853
871
|
const out = (red, facts, r, truncated, parseError) => {
|
|
854
872
|
const { costUsd, unpricedRounds } = r === null ? { costUsd: null, unpricedRounds: 0 } : priceOf(r);
|
|
855
|
-
|
|
873
|
+
// BA-21: the round's provenance rides beside the cost when the loop carried it (see
|
|
874
|
+
// `defaultJudgeLoop`); a loop that never metered a rate adds no key.
|
|
875
|
+
onCost({ costUsd, unpricedRounds, ...(r && typeof r === 'object' && 'rateSource' in r ? { rateSource: r.rateSource } : {}) });
|
|
856
876
|
return {
|
|
857
877
|
ok: red === null, facts, red, costUsd, unpricedRounds, truncated, parseError,
|
|
858
878
|
raw: scrubRaw({
|
package/src/kinds.js
CHANGED
|
@@ -1871,6 +1871,7 @@ async function runJudgedFloor(stage, ctx) {
|
|
|
1871
1871
|
ctx.onJudgeCost({
|
|
1872
1872
|
stage: stage?.name ?? null, kind: stage?.kind ?? null, path: rel, attempt,
|
|
1873
1873
|
label: LOCATE_LABEL, model: judgeModel, costUsd: c.costUsd, unpricedRounds: c.unpricedRounds,
|
|
1874
|
+
...('rateSource' in c ? { rateSource: c.rateSource } : {}),
|
|
1874
1875
|
});
|
|
1875
1876
|
},
|
|
1876
1877
|
});
|
package/src/ledger.js
CHANGED
|
@@ -410,10 +410,9 @@ export function updateLedger({ ledgerFile, spineFiles }) {
|
|
|
410
410
|
// and the plan flow forwards it verbatim onto `worker-round`/`judge-round`/`worker-turn`
|
|
411
411
|
// (`rateSourceFields`, src/planrun.js — the write side).
|
|
412
412
|
//
|
|
413
|
-
//
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
// the day that payload forwards the field the emit site already spreads it.
|
|
413
|
+
// `judge-round`'s payload (src/kinds.js's `onJudgeCost({...})`) forwards `rateSource` from the
|
|
414
|
+
// judge Loop's per-call metering (`defaultJudgeLoop`, src/judged.js); a judge round whose loop
|
|
415
|
+
// carried none — and every judge round archived before this — reads UNKNOWN, by the archive rule.
|
|
417
416
|
//
|
|
418
417
|
// `pricing` is UNCHANGED and still strictly two-valued ('priced'|'unpriced'). Nothing
|
|
419
418
|
// below reads it, nothing below decides anything, and no new escalation category exists:
|
package/src/monthly.js
CHANGED
|
@@ -14,16 +14,16 @@
|
|
|
14
14
|
// run is counted at max(spend so far, capUsd) however quiet its spine; a run with a `job-end` counts
|
|
15
15
|
// its real spend; a claim whose process is gone counts its floor and is settled — with an attributed
|
|
16
16
|
// `settled` entry — by whichever later run finds it (the first note wins). An older row with no `pid` keeps the spine
|
|
17
|
-
// mtime rule (a fresh spine, written to within DIED_MTIME_MS, is in flight)
|
|
17
|
+
// mtime rule (a fresh spine, written to within DIED_MTIME_MS, is in flight) — `runIsAlive`, src/runlist.js, the
|
|
18
|
+
// one "is this run alive" rule the panel's glyph shares. Only the refusal check
|
|
18
19
|
// reserves; the Money tab keeps showing real spend (`usd`).
|
|
19
20
|
//
|
|
20
21
|
// Honesty: a run whose spend is not fully known (a died/still-running spine, an unpriced
|
|
21
22
|
// round, a missing spine file) makes the month total an "at least" figure — never a clean
|
|
22
23
|
// number. The refusal text stays exactly `Max $X (monthly limit)`; `atLeast` travels beside it.
|
|
23
|
-
import { existsSync
|
|
24
|
+
import { existsSync } from 'node:fs';
|
|
24
25
|
import { readConfig, configPath, ConfigError } from './config.js';
|
|
25
|
-
import { readRunList, appendRun, appendRunEvent, isLiveRunner,
|
|
26
|
-
export { isLiveRunner };
|
|
26
|
+
import { readRunList, appendRun, appendRunEvent, isLiveRunner, runIsAlive } from './runlist.js';
|
|
27
27
|
import { parseJsonl } from './replayio.js';
|
|
28
28
|
import { SPEND_RECORD_TYPES, spendProvenance, floorsFromRecords } from './ledger.js';
|
|
29
29
|
import { findRow } from './providerrows.js';
|
|
@@ -141,23 +141,21 @@ export function legTokens(records) {
|
|
|
141
141
|
|
|
142
142
|
/**
|
|
143
143
|
* What the refusal check counts for one run: its spend, unless it is IN FLIGHT (`job-start`, no
|
|
144
|
-
* `job-end`,
|
|
144
|
+
* `job-end`, `runIsAlive` for a row with no pid — the pid-bearing rows are held by `readLegs`), when it is the larger of that
|
|
145
145
|
* spend and the leg's own cap — the signed `budgetUsd` on the spine's `job-start` less the fold
|
|
146
146
|
* a resumed leg inherited (`priorSpentUsd`), the same figure the run-start seam computes
|
|
147
147
|
* (`legCapUsd`, src/userrun.js). A spine that records no `budgetUsd` reserves nothing extra.
|
|
148
148
|
* @param {any[]} records
|
|
149
|
-
* @param {
|
|
149
|
+
* @param {import('./runlist.js').RunRow} row
|
|
150
150
|
* @param {number} nowMs
|
|
151
151
|
* @param {number} usd the leg's spend so far
|
|
152
152
|
* @returns {number}
|
|
153
153
|
*/
|
|
154
|
-
function inFlightCapUsd(records,
|
|
154
|
+
function inFlightCapUsd(records, row, nowMs, usd) {
|
|
155
155
|
if (records.some((r) => r && r.type === 'job-end')) return usd;
|
|
156
156
|
const start = records.find((r) => r && r.type === 'job-start') ?? null;
|
|
157
157
|
if (!start || !(typeof start.budgetUsd === 'number' && Number.isFinite(start.budgetUsd))) return usd;
|
|
158
|
-
|
|
159
|
-
try { mtimeMs = statSync(spinePath).mtimeMs; } catch { return usd; }
|
|
160
|
-
if (nowMs - mtimeMs > DIED_MTIME_MS) return usd; // stale — died, counts its floor
|
|
158
|
+
if (!runIsAlive(row, nowMs)) return usd; // stale — died, counts its floor
|
|
161
159
|
const fold = typeof start.priorSpentUsd === 'number' && Number.isFinite(start.priorSpentUsd) ? start.priorSpentUsd : 0;
|
|
162
160
|
return Math.max(usd, Math.max(0, start.budgetUsd - fold));
|
|
163
161
|
}
|
|
@@ -178,7 +176,7 @@ function rowsAbove(rows, runid) {
|
|
|
178
176
|
/**
|
|
179
177
|
* Every listed run as one {@link Leg}. The ONE reader the month total, the all-time total
|
|
180
178
|
* and the per-provider figures share.
|
|
181
|
-
* @param {{ home?: string, now?: () => number, aboveRunid?: string }} [opts] `now` = the clock for the no-pid in-flight test; `aboveRunid` = read only the runs listed EARLIER in the file than that run (a claim yields to the claims above it, never below)
|
|
179
|
+
* @param {{ home?: string, now?: () => number, aboveRunid?: string, thisMonthOnly?: boolean }} [opts] `now` = the clock for the no-pid in-flight test; `aboveRunid` = read only the runs listed EARLIER in the file than that run (a claim yields to the claims above it, never below); `thisMonthOnly` = do not parse the spine of a run listed in another local month (the refusal check drops it anyway) unless its claim is still held
|
|
182
180
|
* @returns {Leg[]}
|
|
183
181
|
*/
|
|
184
182
|
export function readLegs(opts = {}) {
|
|
@@ -195,7 +193,8 @@ export function readLegs(opts = {}) {
|
|
|
195
193
|
// pid is a live bareloop runner, however quiet its spine — killed = process gone, not silence.
|
|
196
194
|
// An older row with no `pid` keeps the spine-mtime rule (`inFlightCapUsd`), never guessed alive.
|
|
197
195
|
const hasPid = Number.isInteger(row.pid);
|
|
198
|
-
const heldCap = hasPid && !settled.has(row.runid) && typeof row.capUsd === 'number' && Number.isFinite(row.capUsd) &&
|
|
196
|
+
const heldCap = hasPid && !settled.has(row.runid) && typeof row.capUsd === 'number' && Number.isFinite(row.capUsd) && runIsAlive(row, nowMs) ? row.capUsd : null;
|
|
197
|
+
if (opts.thisMonthOnly && heldCap === null && !Number.isNaN(at.getTime()) && !sameLocalMonth(at, new Date(nowMs))) continue;
|
|
199
198
|
if (Number.isNaN(at.getTime()) || !existsSync(row.spine)) { legs.push({ ...base, reservedUsd: heldCap ?? 0 }); continue; }
|
|
200
199
|
/** @type {any[]} */
|
|
201
200
|
let records;
|
|
@@ -217,7 +216,7 @@ export function readLegs(opts = {}) {
|
|
|
217
216
|
vouchedRounds: prov.vouched.rounds,
|
|
218
217
|
otherRounds: prov.guessed.rounds + prov.unpriced.rounds + prov.unknown.rounds,
|
|
219
218
|
reservedUsd: hasPid ? (records.some((r) => r && r.type === 'job-end') || heldCap === null ? leg.usd : Math.max(leg.usd, heldCap))
|
|
220
|
-
: inFlightCapUsd(records, row
|
|
219
|
+
: inFlightCapUsd(records, row, nowMs, leg.usd),
|
|
221
220
|
unreadable: false,
|
|
222
221
|
});
|
|
223
222
|
}
|
|
@@ -233,7 +232,7 @@ const sameLocalMonth = (at, now) => at.getFullYear() === now.getFullYear() && at
|
|
|
233
232
|
|
|
234
233
|
/**
|
|
235
234
|
* @param {{ home?: string, now?: () => number, aboveRunid?: string }} [opts] `aboveRunid`: see {@link readLegs}
|
|
236
|
-
* @returns {{ usd: number, atLeast: boolean, runs: number, reservedUsd: number }} `usd` = real spend (the Money tab's figure); `reservedUsd` = the same with in-flight runs at their cap (the refusal check's)
|
|
235
|
+
* @returns {{ usd: number, atLeast: boolean, runs: number, reservedUsd: number, heldUsd: number, heldRuns: number }} `usd` = real spend (the Money tab's figure); `reservedUsd` = the same with in-flight runs at their cap (the refusal check's); `heldUsd` = what in-progress runs set aside beyond what they have spent (`reservedUsd - usd`), over `heldRuns` runs
|
|
237
236
|
*/
|
|
238
237
|
export function monthSpend(opts = {}) {
|
|
239
238
|
const nowDate = new Date((opts.now ?? Date.now)());
|
|
@@ -241,17 +240,20 @@ export function monthSpend(opts = {}) {
|
|
|
241
240
|
let reservedUsd = 0;
|
|
242
241
|
let atLeast = false;
|
|
243
242
|
let runs = 0;
|
|
244
|
-
|
|
243
|
+
let heldUsd = 0;
|
|
244
|
+
let heldRuns = 0;
|
|
245
|
+
for (const leg of readLegs({ home: opts.home, now: opts.now, aboveRunid: opts.aboveRunid, thisMonthOnly: true })) {
|
|
245
246
|
// an unreadable date could belong to this month — unknown, never dropped
|
|
246
247
|
if (Number.isNaN(leg.at.getTime())) { atLeast = true; continue; }
|
|
247
248
|
if (!sameLocalMonth(leg.at, nowDate)) continue;
|
|
248
249
|
runs += 1;
|
|
249
|
-
if (leg.unreadable) { atLeast = true; reservedUsd += leg.reservedUsd; continue; }
|
|
250
|
+
if (leg.unreadable) { atLeast = true; reservedUsd += leg.reservedUsd; if (leg.reservedUsd > 0) { heldUsd += leg.reservedUsd; heldRuns += 1; } continue; }
|
|
250
251
|
usd += leg.usd;
|
|
251
252
|
reservedUsd += leg.reservedUsd;
|
|
253
|
+
if (leg.reservedUsd > leg.usd) { heldUsd += leg.reservedUsd - leg.usd; heldRuns += 1; }
|
|
252
254
|
if (!leg.complete) atLeast = true;
|
|
253
255
|
}
|
|
254
|
-
return { usd, atLeast, runs, reservedUsd };
|
|
256
|
+
return { usd, atLeast, runs, reservedUsd, heldUsd, heldRuns };
|
|
255
257
|
}
|
|
256
258
|
|
|
257
259
|
/**
|
|
@@ -323,6 +325,8 @@ export function spendSummary(opts = {}) {
|
|
|
323
325
|
* @property {number|null} leftUsd what is left this month (null = no limit set)
|
|
324
326
|
* @property {number|null} limitUsd the person's monthly limit (null = none)
|
|
325
327
|
* @property {boolean} atLeast the month's spend is a floor, so `leftUsd` is a ceiling
|
|
328
|
+
* @property {number} [heldUsd] what run(s) in progress have set aside beyond their spend so far (present when a limit is set)
|
|
329
|
+
* @property {number} [heldRuns] how many runs hold it
|
|
326
330
|
*/
|
|
327
331
|
|
|
328
332
|
/**
|
|
@@ -358,18 +362,21 @@ export function checkMonthlyRoom({ capUsd, home, now, aboveRunid }) {
|
|
|
358
362
|
}
|
|
359
363
|
const spent = monthSpend({ home, now, aboveRunid });
|
|
360
364
|
const leftCents = Math.max(0, Math.floor((limit - spent.reservedUsd) * 100 + 1e-6));
|
|
361
|
-
const capCents = Math.ceil(
|
|
362
|
-
return { ok: capCents <= leftCents, leftUsd: leftCents / 100, limitUsd: limit, atLeast: spent.atLeast };
|
|
365
|
+
const capCents = Math.ceil(capUsd * 100 - 1e-6); // a non-finite cap never fits (NaN/Infinity compare false) — never read as $0
|
|
366
|
+
return { ok: capCents <= leftCents, leftUsd: leftCents / 100, limitUsd: limit, atLeast: spent.atLeast, heldUsd: spent.heldUsd, heldRuns: spent.heldRuns };
|
|
363
367
|
}
|
|
364
368
|
|
|
365
369
|
/**
|
|
366
|
-
* The ONE refusal text, everywhere (CLI, panel cap note, panel Sign refusal).
|
|
370
|
+
* The ONE refusal text, everywhere (CLI, panel cap note, panel Sign refusal). When run(s) in progress hold
|
|
371
|
+
* part of the limit, the same line says how much.
|
|
367
372
|
* @param {MonthlyRoom} room
|
|
368
373
|
* @returns {string|null} null when the run fits
|
|
369
374
|
*/
|
|
370
375
|
export function monthlyRefusalText(room) {
|
|
371
376
|
if (room.ok || room.leftUsd === null) return null;
|
|
372
|
-
|
|
377
|
+
const max = `Max $${room.leftUsd.toFixed(2)}`;
|
|
378
|
+
if (!room.heldRuns || !room.heldUsd || room.heldUsd <= 0) return `${max} (monthly limit)`;
|
|
379
|
+
return `${max} (monthly limit, $${room.heldUsd.toFixed(2)} held by ${room.heldRuns > 1 ? 'runs' : 'a run'} in progress)`;
|
|
373
380
|
}
|
|
374
381
|
|
|
375
382
|
/**
|
|
@@ -34,6 +34,17 @@ export function mintToken() {
|
|
|
34
34
|
return randomBytes(24).toString('hex');
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* The ONE Host check: the request names this panel's own bind address. A page on another
|
|
39
|
+
* origin reaching the panel through a rebound DNS name sends that name as Host, so it fails here.
|
|
40
|
+
* @param {import('node:http').IncomingMessage} req
|
|
41
|
+
* @param {{port: number}} o
|
|
42
|
+
* @returns {{ok: true}|{ok: false, reason: string}}
|
|
43
|
+
*/
|
|
44
|
+
export function checkHostGuard(req, { port }) {
|
|
45
|
+
return req.headers.host === `127.0.0.1:${port}` ? { ok: true } : { ok: false, reason: 'wrong Host' };
|
|
46
|
+
}
|
|
47
|
+
|
|
37
48
|
/**
|
|
38
49
|
* `true` when a request may reach ANY route in this file — the token header
|
|
39
50
|
* plus an Origin/Host that names this exact bind address. `req.headers`
|
|
@@ -48,10 +59,9 @@ export function checkHumanGuard(req, { token, port }) {
|
|
|
48
59
|
if (got !== token) return { ok: false, reason: 'missing or wrong token' };
|
|
49
60
|
const want = `127.0.0.1:${port}`;
|
|
50
61
|
const origin = req.headers.origin;
|
|
51
|
-
const host = req.headers.host;
|
|
52
62
|
if (typeof origin === 'string' && origin.length > 0) {
|
|
53
63
|
if (origin !== `http://${want}` && origin !== `https://${want}`) return { ok: false, reason: 'wrong Origin' };
|
|
54
|
-
} else if (
|
|
64
|
+
} else if (!checkHostGuard(req, { port }).ok) {
|
|
55
65
|
return { ok: false, reason: 'wrong Host (and no Origin header)' };
|
|
56
66
|
}
|
|
57
67
|
return { ok: true };
|
package/src/panel/server.js
CHANGED
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
// for every cap/threshold: a shell never widens what it was asked to do).
|
|
26
26
|
|
|
27
27
|
import { createServer } from 'node:http';
|
|
28
|
-
import { readFileSync, existsSync
|
|
28
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
29
29
|
import {
|
|
30
30
|
dirname, join, basename, relative, isAbsolute, sep,
|
|
31
31
|
} from 'node:path';
|
|
32
32
|
import { fileURLToPath } from 'node:url';
|
|
33
|
-
import { readRunList, DIED_MTIME_MS } from '../runlist.js';
|
|
33
|
+
import { readRunList, DIED_MTIME_MS, runIsAlive } from '../runlist.js';
|
|
34
34
|
import {
|
|
35
35
|
replayOne, parseJsonl, resolveSiblings, isSidecarByName,
|
|
36
36
|
} from '../replayio.js';
|
|
@@ -39,7 +39,7 @@ import { runBehaviour } from '../behaviour.js';
|
|
|
39
39
|
import { SPEND_RECORD_TYPES, floorsFromRecords } from '../ledger.js';
|
|
40
40
|
import { jobSpecHash } from '../job.js';
|
|
41
41
|
import { confirmProtections } from '../authorflow.js';
|
|
42
|
-
import { createAuthorRoutes, mintToken } from './authorroutes.js';
|
|
42
|
+
import { createAuthorRoutes, mintToken, checkHostGuard } from './authorroutes.js';
|
|
43
43
|
import { createSettingsRoutes } from './settingsroutes.js';
|
|
44
44
|
|
|
45
45
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -137,9 +137,9 @@ export function checkTypeTitle(verdictType, atIso) {
|
|
|
137
137
|
return null;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
// DIED (hamr's ruling B, 2026-09-25): a run with no `job-end` is `died` [?], never [✗], once
|
|
141
|
-
//
|
|
142
|
-
//
|
|
140
|
+
// DIED (hamr's ruling B, 2026-09-25): a run with no `job-end` is `died` [?], never [✗], once nothing is
|
|
141
|
+
// running it — `runIsAlive` (src/runlist.js: the row's pid first, the spine-mtime rule DIED_MTIME_MS only
|
|
142
|
+
// for a row with no pid; the monthly limit reads the same rule); the constant is re-exported here for the panel's callers.
|
|
143
143
|
export { DIED_MTIME_MS };
|
|
144
144
|
|
|
145
145
|
/**
|
|
@@ -231,12 +231,12 @@ export function formatTimestamp(ts) {
|
|
|
231
231
|
* fields, which assume a `job-end` exists) — the ONE owner of this
|
|
232
232
|
* derivation, used by every caller below (`/api/runs`, `/api/runs/:id`) so
|
|
233
233
|
* the two never drift apart.
|
|
234
|
-
* @param {
|
|
234
|
+
* @param {import('../runlist.js').RunRow} row the run's listed row (its pid, else its spine's mtime, says whether it is still running)
|
|
235
235
|
* @param {any[]} records raw parsed spine records (already read once by the caller)
|
|
236
236
|
* @param {string|null} outcome `replayRun`'s own `summary.outcome`
|
|
237
237
|
* @returns {{died: boolean, why: string|null, spendFloorUsd: number|null, wallFloorMs: number|null}}
|
|
238
238
|
*/
|
|
239
|
-
function deriveDeath(
|
|
239
|
+
function deriveDeath(row, records, outcome) {
|
|
240
240
|
const notDied = {
|
|
241
241
|
died: false, why: null, spendFloorUsd: null, wallFloorMs: null,
|
|
242
242
|
};
|
|
@@ -245,9 +245,7 @@ function deriveDeath(spinePath, records, outcome) {
|
|
|
245
245
|
// out to be a died run or one that is genuinely still running (build item
|
|
246
246
|
// 6): computed here, once, before the died/still-running branch below.
|
|
247
247
|
const floors = floorsFromRecords(records);
|
|
248
|
-
|
|
249
|
-
try { mtimeMs = statSync(spinePath).mtimeMs; } catch { return { ...notDied, ...floors }; }
|
|
250
|
-
if (Date.now() - mtimeMs <= DIED_MTIME_MS) return { ...notDied, ...floors }; // still fresh — genuinely `running`, not died
|
|
248
|
+
if (runIsAlive(row)) return { ...notDied, ...floors }; // something is still running it — genuinely `running`, not died
|
|
251
249
|
|
|
252
250
|
const withTs = records.filter((r) => r && typeof r === 'object' && typeof r.ts === 'string');
|
|
253
251
|
const last = withTs.length ? withTs[withTs.length - 1] : null;
|
|
@@ -300,7 +298,7 @@ function summarizeRow(row) {
|
|
|
300
298
|
};
|
|
301
299
|
}
|
|
302
300
|
const line = summarizeForAllLine(summary);
|
|
303
|
-
const death = deriveDeath(row
|
|
301
|
+
const death = deriveDeath(row, rawRecords, summary.outcome);
|
|
304
302
|
return {
|
|
305
303
|
runid: row.runid,
|
|
306
304
|
job: row.job,
|
|
@@ -395,7 +393,7 @@ export function getRunDetail(runid, opts = {}) {
|
|
|
395
393
|
const auditPath = resolveAuditPathForRow(row, rawSpineRecords);
|
|
396
394
|
const summary = replayOne(row.spine, { auditPathOverride: auditPath });
|
|
397
395
|
const timelineKind = summary.timelineKind;
|
|
398
|
-
const death = deriveDeath(row
|
|
396
|
+
const death = deriveDeath(row, rawSpineRecords, summary.outcome);
|
|
399
397
|
// judgeModel (Summary box "judge:" line): the FIRST `judge-round`'s own
|
|
400
398
|
// `model` field (src/planrun.js:1704's `onJudgeCost` emit) — a soft-green
|
|
401
399
|
// close's own paid judge seam, distinct from the worker `model` above.
|
|
@@ -1746,6 +1744,13 @@ export function handleRequest(req, res, opts) {
|
|
|
1746
1744
|
}
|
|
1747
1745
|
const { pathname } = url;
|
|
1748
1746
|
|
|
1747
|
+
// every read names the panel's own address as Host — no token needed to read, but another origin's
|
|
1748
|
+
// page (a rebound DNS name) must not be able to fetch the run list
|
|
1749
|
+
if ((method === 'GET' || method === 'HEAD') && !checkHostGuard(req, { port: opts.port }).ok) {
|
|
1750
|
+
sendText(res, 403, 'wrong Host — this panel answers only at its own 127.0.0.1 address');
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1749
1754
|
if (method !== 'GET' && method !== 'HEAD') {
|
|
1750
1755
|
const routes = routesFor(opts, pathname);
|
|
1751
1756
|
if (routes) {
|
package/src/planrun.js
CHANGED
|
@@ -1708,11 +1708,9 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1708
1708
|
stage: c.stage, path: c.path, attempt: c.attempt, label: c.label, model: c.model,
|
|
1709
1709
|
costUsd: c.costUsd, unpricedRounds: c.unpricedRounds,
|
|
1710
1710
|
// BA-21 provenance, through the SAME helper the worker rounds use — this record
|
|
1711
|
-
// is on `spendProvenance`'s read list (src/ledger.js)
|
|
1712
|
-
//
|
|
1713
|
-
//
|
|
1714
|
-
// (src/kinds.js `onJudgeCost({...})`) does not carry `rateSource` today, so it
|
|
1715
|
-
// still reads UNKNOWN — correctly — until that payload forwards it too.
|
|
1711
|
+
// is on `spendProvenance`'s read list (src/ledger.js). The judge cost payload
|
|
1712
|
+
// (src/kinds.js `onJudgeCost`) forwards the field when the judge Loop carried it
|
|
1713
|
+
// (`defaultJudgeLoop`); a loop that metered no rate leaves the key absent (UNKNOWN).
|
|
1716
1714
|
...rateSourceFields(c),
|
|
1717
1715
|
}),
|
|
1718
1716
|
})
|
package/src/providerrows.js
CHANGED
|
@@ -236,12 +236,14 @@ function perMoney(n) {
|
|
|
236
236
|
|
|
237
237
|
/**
|
|
238
238
|
* The one readout line for a resolved customer price, preview and run tail alike:
|
|
239
|
-
* `yours: in $0.006 / out $1.20 per 1M tokens (DEEPSEEK_API_KEY row)`.
|
|
239
|
+
* `yours: in $0.006 / out $1.20 per 1M tokens (DEEPSEEK_API_KEY row)`. A 0 / 0 price is accepted (the price
|
|
240
|
+
* is the customer's) but the line says so: every round then books $0, so spend and limits read $0.
|
|
240
241
|
* @param {{ inPerM: number, outPerM: number, envName: string }} price a {@link ratesFor} result
|
|
241
242
|
* @returns {string}
|
|
242
243
|
*/
|
|
243
244
|
export function priceReadout(price) {
|
|
244
|
-
|
|
245
|
+
const zero = price.inPerM === 0 && price.outPerM === 0 ? ' — WARNING: this price is $0, so spend and limits read $0' : '';
|
|
246
|
+
return `yours: in ${perMoney(price.inPerM)} / out ${perMoney(price.outPerM)} per 1M tokens (${price.envName} row)${zero}`;
|
|
245
247
|
}
|
|
246
248
|
|
|
247
249
|
/**
|
package/src/providers.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// `provider` name to the bare-agent constructor it drives, the env var
|
|
3
3
|
// carrying its key, and the per-tier model table — including any
|
|
4
4
|
// provider-specific request-key gating. Before this module existed, both
|
|
5
|
-
// runners constructed `AnthropicProvider` BY NAME (`
|
|
6
|
-
// `
|
|
5
|
+
// runners constructed `AnthropicProvider` BY NAME (the old `buildProviders` in
|
|
6
|
+
// `src/cli.js` and `scripts/run-u.mjs`'s top-level wiring, both since replaced
|
|
7
|
+
// by `buildRunnerProviders` below) and any second
|
|
7
8
|
// provider would have meant scattering `if (provider === …)` across both.
|
|
8
9
|
//
|
|
9
10
|
// hamr's ruling (PRD 30.7, 2026-09-09): `deepseek-chat` is THE secondary
|
|
@@ -265,8 +266,7 @@ export function makeProvider(providerName, { apiKey, model, baseUrl } = {}) {
|
|
|
265
266
|
|
|
266
267
|
/**
|
|
267
268
|
* Build the `{provider, providerFor, judgeProvider}` triple both runners
|
|
268
|
-
* need (`src/
|
|
269
|
-
* wiring) — ONE seam instead of two copies of the same construction and
|
|
269
|
+
* need (`src/authorrun.js` and `src/panel/authorsession.js` call it) — ONE seam instead of two copies of the same construction and
|
|
270
270
|
* tier-memoization logic. `model`/`tierModels` are already-RESOLVED inputs
|
|
271
271
|
* (the caller runs `resolveWorkerModel` itself, exactly as both runners did
|
|
272
272
|
* before this factory existed — that decision stays where each runner's own
|
package/src/runlist.js
CHANGED
|
@@ -213,6 +213,21 @@ function runidForSpine(spinePath) {
|
|
|
213
213
|
// the panel) because the monthly limit reads the same rule (src/monthly.js).
|
|
214
214
|
export const DIED_MTIME_MS = 10 * 60 * 1000;
|
|
215
215
|
|
|
216
|
+
/**
|
|
217
|
+
* The ONE "is this run alive" rule, for the panel's glyph ([▶] vs [?]) and the monthly limit's hold. A
|
|
218
|
+
* row that carries its runner's `pid` is alive exactly while that pid is a live bareloop runner
|
|
219
|
+
* ({@link isLiveRunner}) however quiet its spine; only a row with NO pid (an older row) falls back to
|
|
220
|
+
* the spine-mtime rule above. A run that has ENDED (a `job-end` on its spine) is a different fact the
|
|
221
|
+
* callers read off the spine itself — this answers "is a process still running it", nothing more.
|
|
222
|
+
* @param {{ pid?: number, spine: string }} row
|
|
223
|
+
* @param {number} [nowMs] the clock for the mtime fallback
|
|
224
|
+
* @returns {boolean}
|
|
225
|
+
*/
|
|
226
|
+
export function runIsAlive(row, nowMs = Date.now()) {
|
|
227
|
+
if (Number.isInteger(row.pid)) return isLiveRunner(/** @type {number} */ (row.pid));
|
|
228
|
+
try { return nowMs - statSync(row.spine).mtimeMs <= DIED_MTIME_MS; } catch { return false; }
|
|
229
|
+
}
|
|
230
|
+
|
|
216
231
|
// A backfill scan of a real patients directory can be arbitrarily deep
|
|
217
232
|
// (person-path runs archive several levels down: `<dir>/<proj>/out/
|
|
218
233
|
// source-<x>/<proj>-bareloop/u-<id>.jsonl`, 4 levels under the dir a person
|
package/src/userrun.js
CHANGED
|
@@ -72,7 +72,7 @@ import { answerReviewDoor, doorRecordOf, doorAgeGate } from './reviewdoor.js';
|
|
|
72
72
|
// the cold reset, shared with the battery drivers so "cold" has one spelling
|
|
73
73
|
import { coldReset, moveStaleGateAudit } from './u-patient.js';
|
|
74
74
|
// PANEL-BUILD.md P1 — the one run list (`~/.config/bareloop/runs.jsonl`).
|
|
75
|
-
import { appendRun, appendRunEvent, isLiveRunner } from './runlist.js';
|
|
75
|
+
import { appendRun, appendRunEvent, isLiveRunner, readRunList } from './runlist.js';
|
|
76
76
|
import { keysForDoor } from './keysfile.js';
|
|
77
77
|
import { parseJsonl } from './replayio.js';
|
|
78
78
|
import { claimRun, monthlyRefusalText, legSpend } from './monthly.js';
|
|
@@ -362,6 +362,13 @@ async function execute(ctx) {
|
|
|
362
362
|
// Set on the no-limit path too, once the row is appended there (the same "listed, no spine yet" state).
|
|
363
363
|
/** @type {{ runid: string, spine: string, home: string|undefined }|null} */
|
|
364
364
|
let claimedRow = null;
|
|
365
|
+
// Release the claimed row (a `released` entry folds it out of the list, like a monthly refusal): the run
|
|
366
|
+
// spent nothing and must not stay a ghost that keeps the month reading "at least". A failed release write
|
|
367
|
+
// never masks the original exit.
|
|
368
|
+
const releaseClaimedRow = () => {
|
|
369
|
+
if (!claimedRow) return;
|
|
370
|
+
try { appendRunEvent({ runid: claimedRow.runid, type: 'released', by: claimedRow.runid, reason: 'not started', at: new Date().toISOString() }, { home: claimedRow.home }); } catch { /* the exit stands; a stranded row of a dead process is closed by the next run */ }
|
|
371
|
+
};
|
|
365
372
|
try {
|
|
366
373
|
const spec = ctx.spec;
|
|
367
374
|
const specPath = ctx.specPath;
|
|
@@ -684,7 +691,7 @@ async function execute(ctx) {
|
|
|
684
691
|
});
|
|
685
692
|
// is the dead run actually dead? Two processes on one patient is unrecoverable; a
|
|
686
693
|
// false refusal costs one sentence. run-u's own pid is not on its spine, so the
|
|
687
|
-
// watchdog's report is the pid there is — and its absence is not evidence of life.
|
|
694
|
+
// watchdog's report or the run-list row is the pid there is — and its absence is not evidence of life.
|
|
688
695
|
const wdFile = `${deadSpineFile}.watchdog.json`;
|
|
689
696
|
/** @type {any} */
|
|
690
697
|
let watchdog = null;
|
|
@@ -692,8 +699,14 @@ async function execute(ctx) {
|
|
|
692
699
|
// ONE owner of "is that pid a live bareloop runner" (src/runlist.js `isLiveRunner`, shared with the
|
|
693
700
|
// monthly limit's holds): alive AND a bareloop runner refuses; a recycled pid that now runs some
|
|
694
701
|
// other program does not. Where /proc is unreadable an existing pid is assumed to be ours.
|
|
695
|
-
|
|
696
|
-
|
|
702
|
+
// The pid is the watchdog's kill record's, or the predecessor's OWN run-list row's (every row carries
|
|
703
|
+
// its runner's pid): a run never killed by the watchdog has no record but still has its row.
|
|
704
|
+
/** @type {number|undefined} */
|
|
705
|
+
let rowPid;
|
|
706
|
+
if (cfgOn) { try { rowPid = readRunList({ home: cfgHome }).rows.findLast((r) => r.spine === deadSpineFile)?.pid; } catch { /* an unreadable list is not evidence of life */ } }
|
|
707
|
+
const alivePid = [watchdog?.pid, rowPid].find((p) => Number.isInteger(p) && isLiveRunner(p));
|
|
708
|
+
if (alivePid !== undefined) {
|
|
709
|
+
die(`--resume: pid ${alivePid} from ${deadSpineFile} is still alive (a bareloop runner). Two processes on one patient is unrecoverable — stop it first.`);
|
|
697
710
|
}
|
|
698
711
|
// WHEN did the dead leg stop? The watchdog's kill record is later, better evidence
|
|
699
712
|
// than the last spine event for a run that was KILLED — and worse evidence for one
|
|
@@ -1704,6 +1717,7 @@ async function execute(ctx) {
|
|
|
1704
1717
|
emit('destination-refused', { code: sourceManifest.code, detail: sourceManifest.stop });
|
|
1705
1718
|
emit('run-end', { outcome: 'escalated' });
|
|
1706
1719
|
err(`SOURCE-MANIFEST-RED — ${sourceManifest.stop}. See ${spineFile}`);
|
|
1720
|
+
releaseClaimedRow(); // a $0 refusal after the spine exists: no ghost row, no "at least" month
|
|
1707
1721
|
throw new ExitSignal(1);
|
|
1708
1722
|
}
|
|
1709
1723
|
/** the front door's per-run instance, or null when there is none (no
|
|
@@ -1722,6 +1736,7 @@ async function execute(ctx) {
|
|
|
1722
1736
|
emit('destination-refused', { code: dp.code, detail: dp.stop });
|
|
1723
1737
|
emit('run-end', { outcome: 'escalated' });
|
|
1724
1738
|
err(`DESTINATION-RED — ${dp.stop}. See ${spineFile}`);
|
|
1739
|
+
releaseClaimedRow(); // a $0 refusal after the spine exists: no ghost row, no "at least" month
|
|
1725
1740
|
throw new ExitSignal(1);
|
|
1726
1741
|
}
|
|
1727
1742
|
}
|
|
@@ -1740,6 +1755,7 @@ async function execute(ctx) {
|
|
|
1740
1755
|
});
|
|
1741
1756
|
emit('run-end', { outcome: 'escalated' });
|
|
1742
1757
|
err(`CLOSE-TIMING-RED — stage(s) never finished the timing preflight: ${names}. See ${spineFile}`);
|
|
1758
|
+
releaseClaimedRow(); // a $0 refusal after the spine exists: no ghost row, no "at least" month
|
|
1743
1759
|
throw new ExitSignal(1);
|
|
1744
1760
|
}
|
|
1745
1761
|
const RESOLVED_CLOSE_TIMEOUT_MS = /** @type {number} */ (closeTimingResolved.closeTimeoutMs);
|
|
@@ -1991,10 +2007,16 @@ async function execute(ctx) {
|
|
|
1991
2007
|
: mh.trend === 'flat' ? 'more money is unlikely to help; this bundle\'s goal is signed and cannot be revised here'
|
|
1992
2008
|
: 'read the last close output before deciding whether more money is worth it';
|
|
1993
2009
|
out(` lever ${lever}`);
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
2010
|
+
if (spentUsd !== null && spentUsd >= signed) {
|
|
2011
|
+
// the whole signed ceiling is spent: "more than spent, at most signed" is unsatisfiable, so no resume line is printed
|
|
2012
|
+
out(` · no room left under the signed ceiling: this run spent all of the signed $${signed}, and a bundle's ceiling cannot be raised`);
|
|
2013
|
+
out(' · abandon the task');
|
|
2014
|
+
} else {
|
|
2015
|
+
out(` · choose a larger --budget on the resume line below (the room left is the signed $${signed}; it cannot be raised past that)`);
|
|
2016
|
+
out(' · abandon the task');
|
|
2017
|
+
out(` resume ${ctx.bundle.invoke}${WALL_FLAG} --resume ${runid}${SHIM_TAIL}${SCOUT_TAIL}${DRAFT_TAIL}${REGISTRY_TAIL} --budget <more than ${spentTxt}, at most $${signed}> --approve ${PRINT_APPROVE}`);
|
|
2018
|
+
out(' (type the --budget number yourself — nothing here fills it in, so this line does not run as printed; nothing in the run may widen its own budget)');
|
|
2019
|
+
}
|
|
1998
2020
|
} else {
|
|
1999
2021
|
out(` lever ${mh.lever}`);
|
|
2000
2022
|
for (const o of mh.options ?? []) out(` · ${o}`);
|
|
@@ -2270,9 +2292,7 @@ async function execute(ctx) {
|
|
|
2270
2292
|
// owns no spine, so its row is released (folded out of the list, like a monthly refusal) rather than
|
|
2271
2293
|
// left as a ghost that keeps the month reading "at least". Once the spine exists the run is a real,
|
|
2272
2294
|
// listed run and ends by its own job-end. A failed release write never masks the original exit.
|
|
2273
|
-
if (claimedRow && !existsSync(claimedRow.spine))
|
|
2274
|
-
try { appendRunEvent({ runid: claimedRow.runid, type: 'released', by: claimedRow.runid, reason: 'not started', at: new Date().toISOString() }, { home: claimedRow.home }); } catch { /* the exit stands; a stranded row of a dead process is closed by the next run */ }
|
|
2275
|
-
}
|
|
2295
|
+
if (claimedRow && !existsSync(claimedRow.spine)) releaseClaimedRow();
|
|
2276
2296
|
if (e instanceof ExitSignal) return e.code;
|
|
2277
2297
|
throw e;
|
|
2278
2298
|
}
|
package/types/judged.d.ts
CHANGED
|
@@ -229,7 +229,7 @@ export function locatePrompt(card: any): string;
|
|
|
229
229
|
* On every route out, red or clean, `onCost` fires exactly once with the honest
|
|
230
230
|
* read: a paid call that leaves no meter record is F12 wearing a judge's coat.
|
|
231
231
|
* @param {{artifactText: string, card: any, loopFactory: (o: {provider?: any, system: string}) => any,
|
|
232
|
-
* maxTokens?: number, attempt?: number, onCost?: (c: {costUsd: number|null, unpricedRounds: number}) => void,
|
|
232
|
+
* maxTokens?: number, attempt?: number, onCost?: (c: {costUsd: number|null, unpricedRounds: number, rateSource?: string|null}) => void,
|
|
233
233
|
* callBounds?: {timeoutMs?: number, deadlineMs?: number}}} o
|
|
234
234
|
* @returns {Promise<{ok: boolean, facts: any|null, red: {axis: string, detail: string}|null,
|
|
235
235
|
* costUsd: number|null, unpricedRounds: number, truncated: boolean, parseError: boolean,
|
|
@@ -247,6 +247,7 @@ export function runLocate({ artifactText, card, loopFactory, maxTokens, attempt,
|
|
|
247
247
|
onCost?: (c: {
|
|
248
248
|
costUsd: number | null;
|
|
249
249
|
unpricedRounds: number;
|
|
250
|
+
rateSource?: string | null;
|
|
250
251
|
}) => void;
|
|
251
252
|
callBounds?: {
|
|
252
253
|
timeoutMs?: number;
|
|
@@ -389,7 +390,9 @@ export function defaultJudgeLoop({ provider, system, rates }: {
|
|
|
389
390
|
in: number;
|
|
390
391
|
out: number;
|
|
391
392
|
} | null;
|
|
392
|
-
}):
|
|
393
|
+
}): {
|
|
394
|
+
run: (msgs: any[], tools: any[], opts?: any) => Promise<any>;
|
|
395
|
+
};
|
|
393
396
|
export type RuleRed = {
|
|
394
397
|
fn: string;
|
|
395
398
|
why: string;
|
package/types/monthly.d.ts
CHANGED
|
@@ -29,17 +29,18 @@ export function legTokens(records: any[]): number;
|
|
|
29
29
|
/**
|
|
30
30
|
* Every listed run as one {@link Leg}. The ONE reader the month total, the all-time total
|
|
31
31
|
* and the per-provider figures share.
|
|
32
|
-
* @param {{ home?: string, now?: () => number, aboveRunid?: string }} [opts] `now` = the clock for the no-pid in-flight test; `aboveRunid` = read only the runs listed EARLIER in the file than that run (a claim yields to the claims above it, never below)
|
|
32
|
+
* @param {{ home?: string, now?: () => number, aboveRunid?: string, thisMonthOnly?: boolean }} [opts] `now` = the clock for the no-pid in-flight test; `aboveRunid` = read only the runs listed EARLIER in the file than that run (a claim yields to the claims above it, never below); `thisMonthOnly` = do not parse the spine of a run listed in another local month (the refusal check drops it anyway) unless its claim is still held
|
|
33
33
|
* @returns {Leg[]}
|
|
34
34
|
*/
|
|
35
35
|
export function readLegs(opts?: {
|
|
36
36
|
home?: string;
|
|
37
37
|
now?: () => number;
|
|
38
38
|
aboveRunid?: string;
|
|
39
|
+
thisMonthOnly?: boolean;
|
|
39
40
|
}): Leg[];
|
|
40
41
|
/**
|
|
41
42
|
* @param {{ home?: string, now?: () => number, aboveRunid?: string }} [opts] `aboveRunid`: see {@link readLegs}
|
|
42
|
-
* @returns {{ usd: number, atLeast: boolean, runs: number, reservedUsd: number }} `usd` = real spend (the Money tab's figure); `reservedUsd` = the same with in-flight runs at their cap (the refusal check's)
|
|
43
|
+
* @returns {{ usd: number, atLeast: boolean, runs: number, reservedUsd: number, heldUsd: number, heldRuns: number }} `usd` = real spend (the Money tab's figure); `reservedUsd` = the same with in-flight runs at their cap (the refusal check's); `heldUsd` = what in-progress runs set aside beyond what they have spent (`reservedUsd - usd`), over `heldRuns` runs
|
|
43
44
|
*/
|
|
44
45
|
export function monthSpend(opts?: {
|
|
45
46
|
home?: string;
|
|
@@ -50,6 +51,8 @@ export function monthSpend(opts?: {
|
|
|
50
51
|
atLeast: boolean;
|
|
51
52
|
runs: number;
|
|
52
53
|
reservedUsd: number;
|
|
54
|
+
heldUsd: number;
|
|
55
|
+
heldRuns: number;
|
|
53
56
|
};
|
|
54
57
|
/**
|
|
55
58
|
* The Money tab's figures: all-time and this-month spend, and per-provider breakdown
|
|
@@ -97,6 +100,8 @@ export function spendSummary(opts?: {
|
|
|
97
100
|
* @property {number|null} leftUsd what is left this month (null = no limit set)
|
|
98
101
|
* @property {number|null} limitUsd the person's monthly limit (null = none)
|
|
99
102
|
* @property {boolean} atLeast the month's spend is a floor, so `leftUsd` is a ceiling
|
|
103
|
+
* @property {number} [heldUsd] what run(s) in progress have set aside beyond their spend so far (present when a limit is set)
|
|
104
|
+
* @property {number} [heldRuns] how many runs hold it
|
|
100
105
|
*/
|
|
101
106
|
/**
|
|
102
107
|
* The person's monthly limit off a parsed config.json: key absent (or null) = no limit (null);
|
|
@@ -122,7 +127,8 @@ export function checkMonthlyRoom({ capUsd, home, now, aboveRunid }: {
|
|
|
122
127
|
aboveRunid?: string;
|
|
123
128
|
}): MonthlyRoom;
|
|
124
129
|
/**
|
|
125
|
-
* The ONE refusal text, everywhere (CLI, panel cap note, panel Sign refusal).
|
|
130
|
+
* The ONE refusal text, everywhere (CLI, panel cap note, panel Sign refusal). When run(s) in progress hold
|
|
131
|
+
* part of the limit, the same line says how much.
|
|
126
132
|
* @param {MonthlyRoom} room
|
|
127
133
|
* @returns {string|null} null when the run fits
|
|
128
134
|
*/
|
|
@@ -160,7 +166,6 @@ export function claimRun({ row, capUsd, home, now }: {
|
|
|
160
166
|
room: MonthlyRoom;
|
|
161
167
|
claimed: boolean;
|
|
162
168
|
};
|
|
163
|
-
export { isLiveRunner };
|
|
164
169
|
export type Leg = {
|
|
165
170
|
/**
|
|
166
171
|
* when the run was listed
|
|
@@ -223,5 +228,12 @@ export type MonthlyRoom = {
|
|
|
223
228
|
* the month's spend is a floor, so `leftUsd` is a ceiling
|
|
224
229
|
*/
|
|
225
230
|
atLeast: boolean;
|
|
231
|
+
/**
|
|
232
|
+
* what run(s) in progress have set aside beyond their spend so far (present when a limit is set)
|
|
233
|
+
*/
|
|
234
|
+
heldUsd?: number | undefined;
|
|
235
|
+
/**
|
|
236
|
+
* how many runs hold it
|
|
237
|
+
*/
|
|
238
|
+
heldRuns?: number | undefined;
|
|
226
239
|
};
|
|
227
|
-
import { isLiveRunner } from './runlist.js';
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
/** @returns {string} a fresh per-process token — never persisted, never logged */
|
|
2
2
|
export function mintToken(): string;
|
|
3
|
+
/**
|
|
4
|
+
* The ONE Host check: the request names this panel's own bind address. A page on another
|
|
5
|
+
* origin reaching the panel through a rebound DNS name sends that name as Host, so it fails here.
|
|
6
|
+
* @param {import('node:http').IncomingMessage} req
|
|
7
|
+
* @param {{port: number}} o
|
|
8
|
+
* @returns {{ok: true}|{ok: false, reason: string}}
|
|
9
|
+
*/
|
|
10
|
+
export function checkHostGuard(req: import("node:http").IncomingMessage, { port }: {
|
|
11
|
+
port: number;
|
|
12
|
+
}): {
|
|
13
|
+
ok: true;
|
|
14
|
+
} | {
|
|
15
|
+
ok: false;
|
|
16
|
+
reason: string;
|
|
17
|
+
};
|
|
3
18
|
/**
|
|
4
19
|
* `true` when a request may reach ANY route in this file — the token header
|
|
5
20
|
* plus an Origin/Host that names this exact bind address. `req.headers`
|
package/types/providerrows.d.ts
CHANGED
|
@@ -127,7 +127,8 @@ export function keyNameFor(provider: string | null | undefined, baseUrl: string
|
|
|
127
127
|
export function applyConfiguredKey(env: Record<string, string | undefined>, provider: string | null | undefined, baseUrl: string | null | undefined, rows: readonly KeyRow[], model?: string | null | undefined): Record<string, string | undefined>;
|
|
128
128
|
/**
|
|
129
129
|
* The one readout line for a resolved customer price, preview and run tail alike:
|
|
130
|
-
* `yours: in $0.006 / out $1.20 per 1M tokens (DEEPSEEK_API_KEY row)`.
|
|
130
|
+
* `yours: in $0.006 / out $1.20 per 1M tokens (DEEPSEEK_API_KEY row)`. A 0 / 0 price is accepted (the price
|
|
131
|
+
* is the customer's) but the line says so: every round then books $0, so spend and limits read $0.
|
|
131
132
|
* @param {{ inPerM: number, outPerM: number, envName: string }} price a {@link ratesFor} result
|
|
132
133
|
* @returns {string}
|
|
133
134
|
*/
|
package/types/providers.d.ts
CHANGED
|
@@ -52,8 +52,7 @@ export function makeProvider(providerName: string, { apiKey, model, baseUrl }?:
|
|
|
52
52
|
}): any;
|
|
53
53
|
/**
|
|
54
54
|
* Build the `{provider, providerFor, judgeProvider}` triple both runners
|
|
55
|
-
* need (`src/
|
|
56
|
-
* wiring) — ONE seam instead of two copies of the same construction and
|
|
55
|
+
* need (`src/authorrun.js` and `src/panel/authorsession.js` call it) — ONE seam instead of two copies of the same construction and
|
|
57
56
|
* tier-memoization logic. `model`/`tierModels` are already-RESOLVED inputs
|
|
58
57
|
* (the caller runs `resolveWorkerModel` itself, exactly as both runners did
|
|
59
58
|
* before this factory existed — that decision stays where each runner's own
|
package/types/runlist.d.ts
CHANGED
|
@@ -95,6 +95,20 @@ export function readRunList(opts?: {
|
|
|
95
95
|
* @returns {boolean}
|
|
96
96
|
*/
|
|
97
97
|
export function isLiveRunner(pid: number): boolean;
|
|
98
|
+
/**
|
|
99
|
+
* The ONE "is this run alive" rule, for the panel's glyph ([▶] vs [?]) and the monthly limit's hold. A
|
|
100
|
+
* row that carries its runner's `pid` is alive exactly while that pid is a live bareloop runner
|
|
101
|
+
* ({@link isLiveRunner}) however quiet its spine; only a row with NO pid (an older row) falls back to
|
|
102
|
+
* the spine-mtime rule above. A run that has ENDED (a `job-end` on its spine) is a different fact the
|
|
103
|
+
* callers read off the spine itself — this answers "is a process still running it", nothing more.
|
|
104
|
+
* @param {{ pid?: number, spine: string }} row
|
|
105
|
+
* @param {number} [nowMs] the clock for the mtime fallback
|
|
106
|
+
* @returns {boolean}
|
|
107
|
+
*/
|
|
108
|
+
export function runIsAlive(row: {
|
|
109
|
+
pid?: number;
|
|
110
|
+
spine: string;
|
|
111
|
+
}, nowMs?: number): boolean;
|
|
98
112
|
/**
|
|
99
113
|
* `bareloop runs backfill <dir>` — recursively scan `dir` (bounded to
|
|
100
114
|
* {@link MAX_BACKFILL_DEPTH} levels, never into `node_modules`/`.git`,
|