clearotron 0.3.2-beta.7 → 0.3.2-beta.8
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/.env.example +24 -23
- package/INSTALL.md +142 -75
- package/README.md +3 -3
- package/bin/onboard.mjs +637 -216
- package/bin/start.mjs +133 -23
- package/bin/update.mjs +58 -11
- package/build-info.json +2 -2
- package/docs/architecture/04-configuration-reference.md +26 -11
- package/docs/architecture/05-config-governance.md +17 -7
- package/driver/CHANGELOG.md +76 -0
- package/driver/band-size.mjs +59 -0
- package/driver/config-inventory.mjs +112 -9
- package/driver/contract-arm2-baseline.json +1 -3
- package/driver/contract-e3-backlog.mjs +26 -26
- package/driver/contract-vocabulary.mjs +44 -10
- package/driver/door-gates.mjs +41 -7
- package/driver/driver.config.mjs +272 -59
- package/driver/engine/CONTRACT.md +10 -3
- package/driver/engine/README.md +2 -2
- package/driver/engine/anthropic-agent.mjs +77 -21
- package/driver/engine/auth.mjs +129 -10
- package/driver/engine/jx-turn.mjs +7 -6
- package/driver/engine/mcp/recording-server.mjs +13 -0
- package/driver/engine/openai-agent.mjs +4 -2
- package/driver/engine/probe.mjs +110 -23
- package/driver/findings-model.mjs +1 -1
- package/driver/flag-snapshot.mjs +28 -5
- package/driver/gateway.mjs +24 -18
- package/driver/jx-lanes.mjs +21 -2
- package/driver/jx-units.mjs +6 -3
- package/driver/jx.mjs +4 -2
- package/driver/matter-frame-record.mjs +90 -1
- package/driver/named-band.mjs +34 -2
- package/driver/package.json +1 -1
- package/driver/pipeline.mjs +200 -23
- package/driver/portal-config-view.mjs +30 -1
- package/driver/portal-report.mjs +15 -1
- package/driver/portal-service.mjs +46 -6
- package/driver/predelivery-lint.mjs +12 -2
- package/driver/publish/index.mjs +46 -5
- package/driver/publish/knockout.mjs +10 -1
- package/driver/publish/render-knockout.mjs +69 -7
- package/driver/publish/render.mjs +170 -59
- package/driver/publish/report-data.mjs +4 -1
- package/driver/publish/report-topbar.mjs +58 -0
- package/driver/publish/templates/report.css +18 -1
- package/driver/publish/xlsx.mjs +13 -1
- package/driver/register-availability.mjs +2 -2
- package/driver/register-coverage.mjs +94 -1
- package/driver/register-digest-record.mjs +236 -11
- package/driver/register-plan.mjs +170 -0
- package/driver/result-noun-fields.mjs +2 -2
- package/driver/run-economics.mjs +41 -10
- package/driver/run-requirements.mjs +173 -9
- package/driver/runner.mjs +3 -3
- package/driver/stages.mjs +12 -8
- package/driver/suite-census.json +142 -64
- package/driver/systemd/README.md +7 -4
- package/driver/terminal-clamp.mjs +107 -1
- package/driver/tokens.mjs +169 -3
- package/driver/unit-environment.mjs +42 -15
- package/driver/unit-inventory.mjs +19 -2
- package/driver/verify.mjs +27 -0
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +15 -1
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-5UyqAyNM.js → index-6jzO9HiX.js} +155 -79
- package/portal-ui/dist/index.html +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/jx/README.md +2 -1
- package/providers/jx/src/turn-envelope.mjs +8 -3
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/uspto-local/README.md +1 -1
- package/scripts/authority-boundary-probe.mjs +4 -2
- package/scripts/env-audit.mjs +12 -6
- package/scripts/freeze-example-run.mjs +49 -16
- package/scripts/generated-files-are-current.mjs +69 -4
- package/scripts/settings-render-check.mjs +75 -2
- package/scripts/test-full.mjs +96 -3
- package/scripts/test-run.mjs +10 -0
- package/shared/deployment-box.mjs +7 -2
- package/shared/driver-dir.mjs +1 -1
- package/shared/names-in-force.mjs +1 -1
package/driver/engine/probe.mjs
CHANGED
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
// The one place the real adapter is exercised, it is pointed at `driver/test/mock-claude.mjs` through
|
|
55
55
|
// `CLEAROTRON_CLAUDE_PATH` — the same offline fixture the engine tests already spawn.
|
|
56
56
|
|
|
57
|
-
import { ENGINE_BINARIES, DEFAULT_ENGINE_ID, engineAdapterSpecifier } from "../driver.config.mjs";
|
|
58
|
-
import { resolveAuthMode } from "./auth.mjs";
|
|
57
|
+
import { ENGINE_BINARIES, DEFAULT_ENGINE_ID, engineAdapterSpecifier, resolveEngineProgram } from "../driver.config.mjs";
|
|
58
|
+
import { resolveAuthMode, CLOUD_SETTINGS, CLOUD_CREDENTIAL_CHECK } from "./auth.mjs";
|
|
59
59
|
|
|
60
60
|
/** Six words. Short enough to be free in practice, and it still requires a real completed turn. */
|
|
61
61
|
export const PROBE_PROMPT = "Reply with the single word: ok.";
|
|
@@ -90,44 +90,97 @@ const tail = (s) => {
|
|
|
90
90
|
return t.length > DETAIL_CHARS ? `…${t.slice(-DETAIL_CHARS)}` : t;
|
|
91
91
|
};
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* An engine instruction that names its program (`run \`claude\` once…`, `claude setup-token`), rewritten to
|
|
95
|
+
* name the copy that will actually run when that copy is the one Clearotron installed. That copy is not on
|
|
96
|
+
* PATH, so for it the bare word is a command the reader's shell cannot
|
|
97
|
+
* find, at the one step nobody can do for them. Any other copy is on PATH or named by path already, and
|
|
98
|
+
* the text is returned unchanged.
|
|
99
|
+
*
|
|
100
|
+
* HERE, AND RE-EXPORTED BY SETUP. Setup's own screens used it and the probe's sign-in advice, which doctor
|
|
101
|
+
* and the run door print, did not, so after setup's install doctor told the reader to run a `claude` their
|
|
102
|
+
* shell does not have. The probe cannot import the wizard, so the one copy lives on this side.
|
|
103
|
+
*/
|
|
104
|
+
export function namingProgram(text, eng, bin) {
|
|
105
|
+
if (!text || bin?.source !== "installed" || !bin.path) return text;
|
|
106
|
+
const program = /\s/.test(bin.path) ? `"${bin.path}"` : bin.path;
|
|
107
|
+
return String(text).replace(new RegExp(`(^|\`)${eng.fallback}(?=[\\s\`]|$)`, "g"), (_, before) => `${before}${program}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
93
110
|
// THE HEADLESS ROUTE, WHERE THE ENGINE HAS ONE. The interactive sign-in is the one thing a server with no
|
|
94
111
|
// browser cannot do, and it was the only remedy this offered — including to a box that had configured the
|
|
95
112
|
// route built for servers. The engine table already carries that route; this reads it rather than a copy.
|
|
96
|
-
|
|
113
|
+
//
|
|
114
|
+
// `program` is the copy that ran ({ source, path }, or null), and every command run ON THIS MACHINE is named
|
|
115
|
+
// through `namingProgram`. The token route's command can run on any machine, so it keeps the bare word,
|
|
116
|
+
// with this machine's copy named beside it, as setup names it.
|
|
117
|
+
const signInLine = (engine, program = null) => {
|
|
97
118
|
const spec = ENGINE_BINARIES[engine];
|
|
98
|
-
const base = spec?.signIn
|
|
119
|
+
const base = spec?.signIn ? namingProgram(spec.signIn, spec, program) : "sign the CLI in";
|
|
99
120
|
const h = spec?.headless;
|
|
100
121
|
// Both forms the wizard already offers, read off the same table: a TOKEN route is run elsewhere and
|
|
101
|
-
// carried here by variable; a DEVICE route is run on this
|
|
122
|
+
// carried here by variable; a DEVICE route is run on this machine and signs it in directly. The route is
|
|
123
|
+
// named by where a sign-in can be completed, as INSTALL.md's sign-in table names it.
|
|
102
124
|
if (!h?.cmd) return base;
|
|
125
|
+
const here = namingProgram(h.cmd, spec, program);
|
|
103
126
|
return h.tokenEnv
|
|
104
|
-
? `${base} — or, on a
|
|
105
|
-
: `${base} — or, on a
|
|
127
|
+
? `${base} — or, on a machine you cannot complete a sign-in on, run \`${h.cmd}\` on any machine you can sign in on${here !== h.cmd ? ` (on this one, \`${here}\`)` : ""} and set the token it prints as ${h.tokenEnv} in this install's environment file`
|
|
128
|
+
: `${base} — or, on a machine you cannot complete a sign-in on, run \`${here}\` here`;
|
|
106
129
|
};
|
|
107
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Who refused the credentials and what to check, for a turn paid through an API key or a cloud account:
|
|
133
|
+
* `{ who, what, check }`, or null on a subscription, whose remedy is the sign-in above. `auth` is the
|
|
134
|
+
* resolver's answer for the turn ({ mode, cloud }). Names only, never a value.
|
|
135
|
+
*
|
|
136
|
+
* THE SIGN-IN IS A SUBSCRIPTION'S REMEDY AND NO OTHER'S. A cloud that refuses the credentials, or a key the
|
|
137
|
+
* vendor refuses, answers 401 or 403 like a signed-out program, and the advice was to run the program once
|
|
138
|
+
* and sign in: nothing to sign in to on a cloud, and a sign-in the adapter would not use under a key.
|
|
139
|
+
*/
|
|
140
|
+
export function credentialCheck(engine, auth) {
|
|
141
|
+
if (auth?.mode === "cloud") {
|
|
142
|
+
const c = CLOUD_CREDENTIAL_CHECK[auth.cloud];
|
|
143
|
+
return c ? { who: c.who, what: "the credentials", check: c.check } : null;
|
|
144
|
+
}
|
|
145
|
+
const spec = ENGINE_BINARIES[engine];
|
|
146
|
+
if (auth?.mode === "api-key" && spec?.apiKeyEnv) return { who: spec.vendor, what: "the API key", check: spec.apiKeyEnv };
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const capitalised = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
150
|
+
|
|
108
151
|
/**
|
|
109
152
|
* One verdict from one turn. PURE — no clock, no filesystem, no process.
|
|
110
153
|
*
|
|
111
154
|
* `tuple` is the engine's normalized return (engine/CONTRACT.md §1); `error` is a THROW, which is a
|
|
112
155
|
* distinct class and not a returned failure: `openai-agent.runTurn` throws for both of its auth shapes
|
|
113
156
|
* (resolveAuthMode on api-key-without-key, and the auth.json refusal) rather than settling a tuple.
|
|
157
|
+
*
|
|
158
|
+
* `auth` is how the turn was paid for (resolveAuthMode's `{ mode, cloud }`) and `program` the copy that ran
|
|
159
|
+
* (`{ source, path }`); both only shape the advice, never the mode, so the run door refuses exactly what it
|
|
160
|
+
* refused before. Absent, the advice is the subscription's, naming the bare program word.
|
|
114
161
|
*/
|
|
115
|
-
export function classifyProbe({ engine, tuple = null, error = null, timeoutSec = PROBE_TIMEOUT_SEC } = {}) {
|
|
162
|
+
export function classifyProbe({ engine, tuple = null, error = null, timeoutSec = PROBE_TIMEOUT_SEC, auth = null, program = null } = {}) {
|
|
116
163
|
const id = String(engine ?? "").trim().toLowerCase();
|
|
117
164
|
const v = (mode, basis, headline, fix, extra = {}) =>
|
|
118
165
|
({ ok: false, engine: id, mode, basis, headline, fix, detail: null, ...extra });
|
|
119
166
|
|
|
120
167
|
// ── a THROW ────────────────────────────────────────────────────────────────────────────────────────
|
|
121
|
-
// The thrown text is relayed
|
|
122
|
-
//
|
|
123
|
-
//
|
|
168
|
+
// The thrown text is relayed as the fix wherever the thrower already says what to do. Those messages
|
|
169
|
+
// were written by the module that owns the decision (auth.mjs owns the billing refusal, the codex
|
|
170
|
+
// adapter owns `codex login`); paraphrasing them here creates a second wording that drifts.
|
|
171
|
+
//
|
|
172
|
+
// ONE CHANGE ONLY, AND ONLY TO A SIGN-IN: the program's name. The codex adapter refuses a subscription
|
|
173
|
+
// with no sign-in by throwing "run `codex login`" before it starts anything, which is the commonest way a
|
|
174
|
+
// signed-out Codex reaches this line. Relayed as thrown, that told a reader whose only copy is the one
|
|
175
|
+
// setup installed, which is not on PATH, to run a command their shell does not have. `namingProgram`
|
|
176
|
+
// rewrites the bare word for that copy and leaves every other copy's text as it was thrown.
|
|
124
177
|
if (error) {
|
|
125
178
|
const msg = String(error?.message ?? error);
|
|
126
|
-
if (/=api-key but/i.test(msg))
|
|
179
|
+
if (error?.billingRefusal === true || /=api-key but/i.test(msg)) // auth.mjs marks every billing refusal
|
|
127
180
|
return v("auth-misconfigured", "config",
|
|
128
|
-
`${id} cannot start: the billing mode this box declares has no key`, msg, { detail: null });
|
|
181
|
+
`${id} cannot start: ${/=api-key but/i.test(msg) ? "the billing mode this box declares has no key" : "the billing setting this box declares is refused"}`, msg, { detail: null });
|
|
129
182
|
if (SIGNED_OUT_RE.test(msg))
|
|
130
|
-
return v("signed-out", "config", `${id} is not signed in`, msg);
|
|
183
|
+
return v("signed-out", "config", `${id} is not signed in`, ENGINE_BINARIES[id] ? namingProgram(msg, ENGINE_BINARIES[id], program) : msg);
|
|
131
184
|
if (TIER_RE.test(msg))
|
|
132
185
|
return v("tier-unavailable", "config", `${id} cannot reach the model it was asked for`, tierFix(id, msg));
|
|
133
186
|
return v("failed", "throw", `${id} could not run a turn`, msg);
|
|
@@ -144,7 +197,11 @@ export function classifyProbe({ engine, tuple = null, error = null, timeoutSec =
|
|
|
144
197
|
// which pipe this happened to look at.
|
|
145
198
|
const detail = tail(tuple.stderr) ?? tail(tuple.stdout);
|
|
146
199
|
|
|
147
|
-
|
|
200
|
+
// A completed turn names what served it, the model and the provider as the program reported them, and
|
|
201
|
+
// null where it named neither, so a proof says which model and whose account it proved.
|
|
202
|
+
if (tuple.code === 0) return { ok: true, engine: id, mode: "ok", basis: "completed-turn", headline: `${id} completed a turn`, fix: null, detail: null,
|
|
203
|
+
served: typeof tuple.modelWire === "string" && tuple.modelWire ? tuple.modelWire : null,
|
|
204
|
+
provider: typeof tuple.providerWire === "string" && tuple.providerWire ? tuple.providerWire : null };
|
|
148
205
|
|
|
149
206
|
// spawn itself failed. The filesystem preflight normally catches this first; when it does not, say so
|
|
150
207
|
// in the binary's own vocabulary rather than as a mysterious engine fault.
|
|
@@ -163,9 +220,15 @@ export function classifyProbe({ engine, tuple = null, error = null, timeoutSec =
|
|
|
163
220
|
{ resetsAt: s.resetsAt ?? null, detail });
|
|
164
221
|
}
|
|
165
222
|
|
|
223
|
+
// THE MODE STAYS `signed-out` UNDER EVERY WAY OF PAYING, because the run door refuses on the mode and a
|
|
224
|
+
// refused credential is as much this machine's to fix as a signed-out program. Only the words follow how
|
|
225
|
+
// the turn is paid for.
|
|
226
|
+
const refused = credentialCheck(id, auth);
|
|
166
227
|
if (SIGNED_OUT_RE.test(text))
|
|
167
|
-
return
|
|
168
|
-
|
|
228
|
+
return refused
|
|
229
|
+
? v("signed-out", "text-match", `${capitalised(refused.who)} refused ${refused.what}`, `check ${refused.check}, then run this again.`, { detail })
|
|
230
|
+
: v("signed-out", "text-match", `${id} is not signed in`,
|
|
231
|
+
`Sign in: ${signInLine(id, program)}, then run this again. Setup does not do it for you — the CLI owns its own login.`, { detail });
|
|
169
232
|
|
|
170
233
|
if (TIER_RE.test(text))
|
|
171
234
|
return v("tier-unavailable", "text-match", `${id} cannot reach the model it was asked for`, tierFix(id, text), { detail });
|
|
@@ -179,7 +242,9 @@ export function classifyProbe({ engine, tuple = null, error = null, timeoutSec =
|
|
|
179
242
|
// from the shape rather than read from a message.
|
|
180
243
|
if (s.noStreamEvents)
|
|
181
244
|
return v("signed-out", "startup-class", `${id} exited before it produced anything`,
|
|
182
|
-
|
|
245
|
+
refused
|
|
246
|
+
? `That is what refused credentials look like, so start there: check ${refused.check}, then run this again. The engine's stderr below is the diagnosis if it is something else.`
|
|
247
|
+
: `That is the signed-out shape, so start there: ${signInLine(id, program)}, then run this again. The engine's stderr below is the diagnosis if it is something else.`, { detail });
|
|
183
248
|
|
|
184
249
|
return v("failed", "nonzero-exit", `${id} ran but the turn failed (exit ${tuple.code})`,
|
|
185
250
|
"The engine's stderr below is the whole story; a turn that starts and fails is not a configuration this check can name.", { detail });
|
|
@@ -292,11 +357,15 @@ export function probeWeatherWarning(verdict) {
|
|
|
292
357
|
* and a caller building the environment it hands this module fills from it. `doctor` kept a second copy
|
|
293
358
|
* that had dropped both credentials, so it filled a probe environment without the very token it then
|
|
294
359
|
* reported missing. Exported so there is nothing left to copy.
|
|
360
|
+
*
|
|
361
|
+
* The cloud a Claude turn is sent to and paid through is on it too (`CLOUD_SETTINGS`, from auth.mjs).
|
|
362
|
+
* Without those names a cloud answered in setup would pass the resolver, which reads the caller's
|
|
363
|
+
* environment, and never reach the turn, which reads this process's: a proof of an account no run bills.
|
|
295
364
|
*/
|
|
296
365
|
export function engineEnvKeys() {
|
|
297
366
|
return [...new Set(["CLEAROTRON_AI", ...Object.values(ENGINE_BINARIES)
|
|
298
367
|
.flatMap((s) => [s.env, s.authEnv, s.apiKeyEnv, s.headless?.tokenEnv])
|
|
299
|
-
.filter(Boolean)])];
|
|
368
|
+
.filter(Boolean), ...CLOUD_SETTINGS])];
|
|
300
369
|
}
|
|
301
370
|
|
|
302
371
|
function applyEngineEnv(env) {
|
|
@@ -346,6 +415,12 @@ async function defaultLoadAdapter(engine) {
|
|
|
346
415
|
/**
|
|
347
416
|
* Run the probe and return a verdict. Never throws for a configuration fault — a caller that wants a
|
|
348
417
|
* refusal calls `preflightEngineTurn`, and a caller that wants to report calls this.
|
|
418
|
+
*
|
|
419
|
+
* `program` is the copy the caller has already found and is running (`{ source, path }`), for a caller
|
|
420
|
+
* that knows more about it than its setting says. Setup pins the program setting to the absolute path of
|
|
421
|
+
* the copy it proves, so the turn runs exactly that copy; resolved from that setting alone, the copy setup
|
|
422
|
+
* installed reads as a path the reader set, and the advice then named the bare word that copy does not
|
|
423
|
+
* answer to. Without it, the probe resolves the copy itself.
|
|
349
424
|
*/
|
|
350
425
|
export async function probeEngineTurn({
|
|
351
426
|
env = process.env,
|
|
@@ -354,6 +429,7 @@ export async function probeEngineTurn({
|
|
|
354
429
|
loadAdapter = defaultLoadAdapter,
|
|
355
430
|
timeoutSec = PROBE_TIMEOUT_SEC,
|
|
356
431
|
stallSec = PROBE_STALL_SEC,
|
|
432
|
+
program: knownProgram = null,
|
|
357
433
|
} = {}) {
|
|
358
434
|
const id = String(env.CLEAROTRON_AI || DEFAULT_ENGINE_ID).trim().toLowerCase();
|
|
359
435
|
if (!ENGINE_BINARIES[id]) {
|
|
@@ -366,8 +442,10 @@ export async function probeEngineTurn({
|
|
|
366
442
|
};
|
|
367
443
|
}
|
|
368
444
|
|
|
369
|
-
// The billing-mode door, before anything spawns — a fail-loud config error must not cost a turn.
|
|
370
|
-
|
|
445
|
+
// The billing-mode door, before anything spawns — a fail-loud config error must not cost a turn. Its
|
|
446
|
+
// answer is kept: a refusal from a cloud or of a key is advised on differently from a signed-out program.
|
|
447
|
+
let auth;
|
|
448
|
+
try { auth = resolveAuthMode({ engineName: id, env }); }
|
|
371
449
|
catch (e) { return classifyProbe({ engine: id, error: e, timeoutSec }); }
|
|
372
450
|
|
|
373
451
|
let turn = injectedRunTurn;
|
|
@@ -377,11 +455,20 @@ export async function probeEngineTurn({
|
|
|
377
455
|
}
|
|
378
456
|
|
|
379
457
|
const restore = applyEngineEnv(env);
|
|
458
|
+
let program = null;
|
|
380
459
|
try {
|
|
460
|
+
// THE COPY THAT RUNS, resolved the way the adapter resolves it: by the one resolver, inside the
|
|
461
|
+
// environment the turn runs in. The sign-in advice names it when it is the copy Clearotron installed,
|
|
462
|
+
// which is not on PATH. AFTER applyEngineEnv, NEVER BEFORE: the resolver reads this process's
|
|
463
|
+
// environment, and the caller's program setting is only there from that line until `restore()`, so above
|
|
464
|
+
// it this would name whatever copy the shell happened to point at. Inside the try, so the `finally` puts
|
|
465
|
+
// the environment back whatever it does. A resolver that cannot answer leaves the bare word.
|
|
466
|
+
if (knownProgram?.path) program = { source: knownProgram.source ?? null, path: knownProgram.path };
|
|
467
|
+
else try { const r = resolveEngineProgram(id); program = r.resolved ? { source: r.source, path: r.resolved } : null; } catch { /* the bare word */ }
|
|
381
468
|
const tuple = await turn({ message: PROBE_PROMPT, model: PROBE_MODEL, thinking: PROBE_THINKING, timeoutSec, stallSec });
|
|
382
|
-
return classifyProbe({ engine: id, tuple, timeoutSec });
|
|
469
|
+
return classifyProbe({ engine: id, tuple, timeoutSec, auth, program });
|
|
383
470
|
} catch (e) {
|
|
384
|
-
return classifyProbe({ engine: id, error: e, timeoutSec });
|
|
471
|
+
return classifyProbe({ engine: id, error: e, timeoutSec, auth, program });
|
|
385
472
|
} finally {
|
|
386
473
|
restore();
|
|
387
474
|
}
|
|
@@ -1732,7 +1732,7 @@ function validateNet(f, ord, mode) {
|
|
|
1732
1732
|
// This file's header says `findings_` = a top-level shape defect, `finding_` = a specific finding/field,
|
|
1733
1733
|
// and by that rule this token would be `finding_net_chained` alongside finding_net_missing /
|
|
1734
1734
|
// finding_net_invalid / finding_net_prescriptive. It is `findings_net_chained` instead, because the
|
|
1735
|
-
// convention is about ROUTING and routing disagrees. pipeline.mjs
|
|
1735
|
+
// convention is about ROUTING and routing disagrees. pipeline.mjs reads:
|
|
1736
1736
|
//
|
|
1737
1737
|
// const eligible = /^invalid_file:/.test(fail) && /:finding_[a-z]/.test(fail) && !/:findings_/.test(fail);
|
|
1738
1738
|
//
|
package/driver/flag-snapshot.mjs
CHANGED
|
@@ -129,7 +129,7 @@ const truthy = (v) => ["1", "true", "yes", "on"].includes(String(v ?? "").trim()
|
|
|
129
129
|
* `capturedAt` is supplied rather than read from the clock so this stays testable and so a caller can
|
|
130
130
|
* stamp it from the same instant it stamps everything else.
|
|
131
131
|
*/
|
|
132
|
-
export function buildFlagSnapshot(env, { capturedAt, registerProvider = null, registerCanCount = null, registerTerritories = undefined, engine = undefined, providers = undefined }) {
|
|
132
|
+
export function buildFlagSnapshot(env, { capturedAt, registerProvider = null, registerLabel = null, registerCanCount = null, registerTerritories = undefined, engine = undefined, providers = undefined }) {
|
|
133
133
|
const flags = {};
|
|
134
134
|
// Written unconditionally, true or false to a count: a reader must be able to tell "this snapshot
|
|
135
135
|
// tracks no flags" from "this snapshot lost its flags", and `flags: {}` alone cannot say which.
|
|
@@ -181,6 +181,12 @@ export function buildFlagSnapshot(env, { capturedAt, registerProvider = null, re
|
|
|
181
181
|
register: registerProvider
|
|
182
182
|
? {
|
|
183
183
|
provider: registerProvider,
|
|
184
|
+
// The register's own DISPLAY label ("Signa"), beside the key the engine switches on ("signa").
|
|
185
|
+
// A door that names the register to a client must not print the key, and capabilities — where
|
|
186
|
+
// the label lives — is a provider module this process may not be able to import. Same split as
|
|
187
|
+
// `territories` directly below: the writer runs in the engine environment and resolves it once.
|
|
188
|
+
// Omitted rather than guessed when the writer had none, and every reader falls back to the key.
|
|
189
|
+
...(registerLabel ? { label: registerLabel } : {}),
|
|
184
190
|
canCount: registerCanCount,
|
|
185
191
|
...(registerTerritories === undefined ? {} : { territories: registerTerritories }),
|
|
186
192
|
}
|
|
@@ -335,9 +341,9 @@ export function postureDisagreement(snapshot, live) {
|
|
|
335
341
|
// the better answer for a reader: "found" against "not found" says it without a legend.
|
|
336
342
|
const found = (v) => (v === true ? "found" : v === false ? "not found" : null);
|
|
337
343
|
differ("engine program", found(snapshot.engine?.binaryPresent), found(live.engine?.binaryPresent),
|
|
338
|
-
"whether a NEW search can start — the
|
|
339
|
-
+ "engine program can be found, so one screen offers a search the other refuses. Restart the
|
|
340
|
-
+ "
|
|
344
|
+
"whether a NEW search can start — the services, when they last started, and this deployment do not agree "
|
|
345
|
+
+ "that the engine program can be found, so one screen offers a search the other refuses. Restart the "
|
|
346
|
+
+ "services so they look again; if they still disagree, `clearotron doctor` says which side to fix and how");
|
|
341
347
|
|
|
342
348
|
// Flags: compare only names BOTH sides declare, for the same reason `differ` skips absent values —
|
|
343
349
|
// a build that adds a flag must not read as every older capture disagreeing with it.
|
|
@@ -425,6 +431,22 @@ export function registerTerritoriesFor(snapshot) {
|
|
|
425
431
|
return Array.isArray(v) ? v.filter((n) => typeof n === "string") : undefined;
|
|
426
432
|
}
|
|
427
433
|
|
|
434
|
+
/**
|
|
435
|
+
* The wired register's DISPLAY label, for a sentence a client reads — `null` when the snapshot does not
|
|
436
|
+
* carry one, which every snapshot written before this shipped does not.
|
|
437
|
+
*
|
|
438
|
+
* FALLS BACK TO THE PROVIDER KEY rather than to nothing: a door that names the register is better off
|
|
439
|
+
* saying "signa" than saying nothing at all, and the caller decides whether a key is good enough to
|
|
440
|
+
* print. It is deliberately NOT title-cased on the way out — "uspto-local" title-cased is worse prose
|
|
441
|
+
* than the key, and inventing a display name is the provider module's job, not this reader's.
|
|
442
|
+
*/
|
|
443
|
+
export function registerLabelFor(snapshot) {
|
|
444
|
+
const l = snapshot?.register?.label;
|
|
445
|
+
if (typeof l === "string" && l.trim()) return l.trim();
|
|
446
|
+
const p = snapshot?.register?.provider;
|
|
447
|
+
return typeof p === "string" && p.trim() ? p.trim() : null;
|
|
448
|
+
}
|
|
449
|
+
|
|
428
450
|
/**
|
|
429
451
|
* What this instance searches. THREE answers, exactly as `registerTerritoriesFor` above:
|
|
430
452
|
*
|
|
@@ -535,11 +557,12 @@ export async function livePosture({ env = process.env } = {}) {
|
|
|
535
557
|
const { REGISTER_PROVIDER } = await import("./driver.config.mjs");
|
|
536
558
|
const { capabilitiesFor } = await import("./register-capabilities.mjs");
|
|
537
559
|
const canCount = (() => { try { return capabilitiesFor(REGISTER_PROVIDER).countProbe !== "none"; } catch { return null; } })();
|
|
560
|
+
const label = (() => { try { return capabilitiesFor(REGISTER_PROVIDER).label ?? null; } catch { return null; } })();
|
|
538
561
|
const { coveredTerritoryNames } = await import("./register-coverage.mjs");
|
|
539
562
|
const territories = await (async () => { try { return await coveredTerritoryNames(capabilitiesFor(REGISTER_PROVIDER)); } catch { return undefined; } })();
|
|
540
563
|
const { engineInventory, providerInventory } = await import("./config-inventory.mjs");
|
|
541
564
|
return buildFlagSnapshot(env, {
|
|
542
|
-
capturedAt: new Date().toISOString(), registerProvider: REGISTER_PROVIDER, registerCanCount: canCount,
|
|
565
|
+
capturedAt: new Date().toISOString(), registerProvider: REGISTER_PROVIDER, registerLabel: label, registerCanCount: canCount,
|
|
543
566
|
registerTerritories: territories,
|
|
544
567
|
engine: engineInventory(env), providers: providerInventory(env),
|
|
545
568
|
});
|
package/driver/gateway.mjs
CHANGED
|
@@ -817,7 +817,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
817
817
|
followup = false, // #5b: this run is a warm-resume / followup (escalation, envelope close, frame-reopen
|
|
818
818
|
// sweep) — a hard-wall timeout breaks after ONE attempt (a 1.5× extension can't fit
|
|
819
819
|
// an already-over-budget resume; the caller records the coverage-limited deferral).
|
|
820
|
-
excludeTools, bandSize, // copper-lattice re-route: tool names dropped from this stage's allowedTools
|
|
820
|
+
excludeTools, bandSize, derivedLimit = null, // copper-lattice re-route: tool names dropped from this stage's allowedTools
|
|
821
821
|
} = opts;
|
|
822
822
|
if (!message) throw new Error(`runStage(${name}): message is required`);
|
|
823
823
|
if (!sessionKey) throw new Error(`runStage(${name}): sessionKey is required`);
|
|
@@ -1145,17 +1145,17 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1145
1145
|
// it; the log was already saying what the code believed.
|
|
1146
1146
|
//
|
|
1147
1147
|
// TWO FIELDS, NEVER COLLAPSED INTO ONE:
|
|
1148
|
-
// modelUsed — the requested resolution
|
|
1149
|
-
//
|
|
1150
|
-
//
|
|
1151
|
-
//
|
|
1152
|
-
//
|
|
1153
|
-
//
|
|
1154
|
-
//
|
|
1155
|
-
// state in which a requested value is dressed as an observed one.
|
|
1148
|
+
// modelUsed — the requested resolution, unchanged in meaning and value: run-economics.mjs and
|
|
1149
|
+
// tokens.mjs both read it, and a field that quietly changes meaning is its own corruption.
|
|
1150
|
+
// modelActual — the id the WIRE reported (engine tuple `modelWire`), or NULL when the stream never
|
|
1151
|
+
// said: an engine that does not emit one (codex), a turn killed before any event, a
|
|
1152
|
+
// spawn error. It NEVER falls back to the requested alias, and `modelBasis` ("actual" or
|
|
1153
|
+
// "unknown") never dresses a requested value as an observed one. `providerReported` is
|
|
1154
|
+
// the provider word the same stream gave (tuple `providerWire`), null on the same terms.
|
|
1156
1155
|
const modelRequested = engine.resolveModelId ? engine.resolveModelId(model) : resolveModel(model);
|
|
1157
1156
|
const modelActual = (typeof turn.modelWire === "string" && turn.modelWire) ? turn.modelWire : null;
|
|
1158
1157
|
const modelBasis = modelActual ? "actual" : "unknown";
|
|
1158
|
+
const providerReported = (typeof turn.providerWire === "string" && turn.providerWire) ? turn.providerWire : null;
|
|
1159
1159
|
// WHETHER THE OBSERVED ID NAMES A FIXED BUILD. `modelBasis: "actual"` says the provider answered,
|
|
1160
1160
|
// not that the answer is pinned: two of the three tiers come back as undated aliases the provider
|
|
1161
1161
|
// may repoint, and recorded beside a dated one they read identically. null when there is nothing to
|
|
@@ -1171,9 +1171,13 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1171
1171
|
// "written before anybody asked", which is the distinction the field exists for. One spawn per
|
|
1172
1172
|
// binary per process; a probe never throws, because taking down a dispatch to record a version
|
|
1173
1173
|
// would be a worse defect than the gap it closes.
|
|
1174
|
+
// `source` says WHICH copy served (explicit / path / installed), and it is attached here, outside the
|
|
1175
|
+
// probe's cache: that cache is keyed by the file, and one file can be reached by more than one route.
|
|
1174
1176
|
const cli = (() => {
|
|
1175
|
-
try {
|
|
1176
|
-
|
|
1177
|
+
try {
|
|
1178
|
+
const pre = preflightEngineBinary(process.env);
|
|
1179
|
+
return { ...probeCliVersion(pre?.resolved ?? null), source: pre?.source ?? null };
|
|
1180
|
+
} catch (e) { return { version: null, probe: "unreadable", why: String(e?.message ?? e).slice(0, 160), source: null }; }
|
|
1177
1181
|
})();
|
|
1178
1182
|
if (modelActual) lastModelWire = modelActual; // — never overwritten with null
|
|
1179
1183
|
// The comparison is by FAMILY (driver.config modelFamily), because `--model haiku` legitimately comes
|
|
@@ -1294,7 +1298,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1294
1298
|
attempt, key, agent, model,
|
|
1295
1299
|
modelUsed: (lastModelUsed = engine.resolveModelId ? engine.resolveModelId(model) : resolveModel(model)),
|
|
1296
1300
|
// Same billing stamp as the attempt row, written on the same terms — see the note there.
|
|
1297
|
-
engine: engine.name, writeBoundary: writeBoundaryOf(engine), authMode: auth.mode, apiBilled: auth.apiBilled === true,
|
|
1301
|
+
engine: engine.name, writeBoundary: writeBoundaryOf(engine), authMode: auth.mode, apiBilled: auth.apiBilled === true, cloud: auth.cloud ?? null,
|
|
1298
1302
|
code: rt.code, wall: rt.wall, timeoutSec: effTimeout,
|
|
1299
1303
|
// — same rename as the attempt row above. This row already carries the driver's verdict as
|
|
1300
1304
|
// `repairOutcome` (only "repaired" is success), so it needs no `ok`; what it lacked was any mark
|
|
@@ -1638,8 +1642,8 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1638
1642
|
// modelMismatch — true/false when both sides name a family, null when either does not.
|
|
1639
1643
|
// Written even on the rows where they are null, so "this engine cannot report" stays visibly
|
|
1640
1644
|
// different from "this record predates the gauge".
|
|
1641
|
-
modelActual, modelBasis, modelSnapshot, modelMismatch,
|
|
1642
|
-
cliVersion: cli.version, cliVersionProbe: cli.probe, ...(cli.why ? { cliVersionWhy: cli.why } : {}),
|
|
1645
|
+
modelActual, modelBasis, modelSnapshot, modelMismatch, providerReported,
|
|
1646
|
+
cliVersion: cli.version, cliVersionProbe: cli.probe, ...(cli.why ? { cliVersionWhy: cli.why } : {}), cliSource: cli.source,
|
|
1643
1647
|
// W3 billing telemetry: which engine ran + the RESOLVED billing mode (subscription vs api-key). This
|
|
1644
1648
|
// records INTENT (the mode the engine was configured to bill under), not independent billing evidence
|
|
1645
1649
|
// — the actual proof is the provider console (claude's stream also reports apiKeySource; codex does
|
|
@@ -1653,7 +1657,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1653
1657
|
// "every telemetry field is written unconditionally, so 'did not happen' stays distinguishable
|
|
1654
1658
|
// from 'not recorded'" (instrumentation-house-rule.test.mjs). A run must be able to STATE that it
|
|
1655
1659
|
// billed subscription, not merely fail to state that it billed API.
|
|
1656
|
-
engine: engine.name, writeBoundary: writeBoundaryOf(engine), authMode: auth.mode, apiBilled: auth.apiBilled === true,
|
|
1660
|
+
engine: engine.name, writeBoundary: writeBoundaryOf(engine), authMode: auth.mode, apiBilled: auth.apiBilled === true, cloud: auth.cloud ?? null,
|
|
1657
1661
|
// build 2 — THIS attempt is the fresh dispatch bought by discarding a warm session that
|
|
1658
1662
|
// reproduced its own failure. Exact, not cumulative: a `warmEscalatedAt > 0` test would mark
|
|
1659
1663
|
// every later attempt too the moment the ladder is deepened, and the row would stop meaning
|
|
@@ -1743,6 +1747,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1743
1747
|
// null (not 0, and not absent) on an engine that cannot report them, so "this adapter does not
|
|
1744
1748
|
// measure" stays visibly different from "this turn called no tools" — see toolGauge.
|
|
1745
1749
|
...toolGauge(turn), band: bandSize ?? undefined,
|
|
1750
|
+
inputBytes: derivedLimit?.inputBytes ?? undefined, derivedLimitSec: derivedLimit?.sec ?? undefined,
|
|
1746
1751
|
// AD-4 emitted-vs-landed, UNCONDITIONAL (was success-only, which made a failed attempt's mid-write
|
|
1747
1752
|
// artifact invisible): `output` = what LANDED on disk after this attempt (null when the stage has no
|
|
1748
1753
|
// expected file); `wrote` = whether THIS attempt emitted it (see the computation above the runDir
|
|
@@ -1776,8 +1781,8 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1776
1781
|
event: "attempt", stage: name, attempt, of: maxRetries + 1, ok: !fail, fail: fail ?? null,
|
|
1777
1782
|
//: the spine carries the same pair as the per-stage log, or the two disagree about what
|
|
1778
1783
|
// ran. `model` stays the requested resolution (its existing readers); `modelActual` is the wire.
|
|
1779
|
-
model: modelRequested, modelActual, modelBasis, modelSnapshot, modelMismatch,
|
|
1780
|
-
cliVersion: cli.version, cliVersionProbe: cli.probe, ...(cli.why ? { cliVersionWhy: cli.why } : {}),
|
|
1784
|
+
model: modelRequested, modelActual, modelBasis, modelSnapshot, modelMismatch, providerReported,
|
|
1785
|
+
cliVersion: cli.version, cliVersionProbe: cli.probe, ...(cli.why ? { cliVersionWhy: cli.why } : {}), cliSource: cli.source,
|
|
1781
1786
|
wrote, warm: warm || undefined, warmEscalated: attempt === warmEscalatedAt || undefined,
|
|
1782
1787
|
rescued: rescued ?? undefined, killed: killed || undefined,
|
|
1783
1788
|
quiescentMs: Number.isFinite(quiescentMs) ? Math.round(quiescentMs) : undefined, // — see the per-stage row
|
|
@@ -1789,7 +1794,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1789
1794
|
// archived runs actually reads, and the question "has this box ever billed API" could not be
|
|
1790
1795
|
// answered from it because the pair was only ever on the per-stage log. Written unconditionally,
|
|
1791
1796
|
// like everything else here: a subscription run states `false`.
|
|
1792
|
-
authMode: auth.mode, apiBilled: auth.apiBilled === true,
|
|
1797
|
+
authMode: auth.mode, apiBilled: auth.apiBilled === true, cloud: auth.cloud ?? null,
|
|
1793
1798
|
//: the spine carries the POINTER and the sha, not the text — enough to find the file and
|
|
1794
1799
|
// to tell two attempts apart without opening either.
|
|
1795
1800
|
dispatch: dispatch?.file ?? null, dispatchSha: dispatch?.sha ?? null,
|
|
@@ -1806,6 +1811,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1806
1811
|
wall, outputTokens: usage?.output ?? null, tokensPerSec: tokensPerSec(usage, wall),
|
|
1807
1812
|
// — the spine carries them too, or a round has to join two files to ask why a stage was slow.
|
|
1808
1813
|
...toolGauge(turn), band: bandSize ?? undefined,
|
|
1814
|
+
inputBytes: derivedLimit?.inputBytes ?? undefined, derivedLimitSec: derivedLimit?.sec ?? undefined,
|
|
1809
1815
|
formRepairs: formRepairsThisAttempt || undefined, //, see the stage row above
|
|
1810
1816
|
});
|
|
1811
1817
|
} catch { /* telemetry best-effort — never fail a turn over a journal line */ }
|
package/driver/jx-lanes.mjs
CHANGED
|
@@ -280,6 +280,25 @@ export const jxBillingStamp = (executorSource, result = null) => {
|
|
|
280
280
|
// NO VENDOR ON THE RESULT MEANS NO DISPATCH HAPPENED. A fixture, an injected executor, or a
|
|
281
281
|
// configuration the engine door refused all return without one, and none of them is provider-billed.
|
|
282
282
|
// Saying so in its own words beats "unknown", which is indistinguishable from an unstamped legacy row.
|
|
283
|
-
if (executorSource !== "engine" || !result?.vendor) return { engine: "not-provider-billed", authMode: "not-provider-billed" };
|
|
284
|
-
return { engine: result.vendor, authMode: result.authMode ?? "not-provider-billed" };
|
|
283
|
+
if (executorSource !== "engine" || !result?.vendor) return { engine: "not-provider-billed", authMode: "not-provider-billed", cloud: null };
|
|
284
|
+
return { engine: result.vendor, authMode: result.authMode ?? "not-provider-billed", cloud: result.cloud ?? null }; // cloud: which account a cloud mode bills
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// ── What a jx ledger row says about the model ─────────────────────────────────────────────────────────
|
|
288
|
+
// The `model` on these rows is the id the program reported for the turn (jx-turn.mjs reads it off the
|
|
289
|
+
// wire), not a tier asked for, so a row that names one also carries it as `modelActual`, the name every
|
|
290
|
+
// attempt row uses for a served id. That is what the report's list of models reads; without it, a model
|
|
291
|
+
// that did only this work was left off the report.
|
|
292
|
+
//
|
|
293
|
+
// A TURN THAT RAN AND NAMED NO MODEL STILL RAN. The Claude program answered it itself, or the stream never
|
|
294
|
+
// said, or the turn was killed first: the program reports no id. Such a row used to carry neither field,
|
|
295
|
+
// and every reader took a row with no `model` for a call that was never made, so the turn's attempt and
|
|
296
|
+
// any tokens it reported fell out of the run's totals, and a run made only of such turns read as one where
|
|
297
|
+
// nothing was looked at. It now records `modelActual: null`, the stage rows' own words for "a turn ran and
|
|
298
|
+
// named no model", and tokens.mjs counts it as an attempt. Only a real dispatch writes it, by the rule
|
|
299
|
+
// jxBillingStamp states: no vendor on the result means no dispatch, so a fixture, an injected executor or
|
|
300
|
+
// a configuration the engine door refused writes neither field.
|
|
301
|
+
export const jxModelFields = (executorSource, result = null) => {
|
|
302
|
+
if (result?.model) return { model: result.model, modelActual: result.model };
|
|
303
|
+
return jxBillingStamp(executorSource, result).engine === "not-provider-billed" ? {} : { modelActual: null };
|
|
285
304
|
};
|
package/driver/jx-units.mjs
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
import { readFileSync, writeFileSync, renameSync, appendFileSync, mkdirSync, existsSync } from "node:fs";
|
|
35
35
|
import { join } from "node:path";
|
|
36
36
|
import { driverDir } from "../shared/driver-dir.mjs"; //
|
|
37
|
-
import { LANGUAGE_LANES, SERP_LANES, isMirrorHost, canonicalTerm, jxBillingStamp } from "./jx-lanes.mjs";
|
|
37
|
+
import { LANGUAGE_LANES, SERP_LANES, isMirrorHost, canonicalTerm, jxBillingStamp, jxModelFields } from "./jx-lanes.mjs";
|
|
38
38
|
import { jxKey, MAX_LANE_ATTEMPTS } from "./jx.mjs";
|
|
39
39
|
import { abbrev } from "./repair-contract.mjs";
|
|
40
40
|
import { kebab } from "./search-policy.mjs";
|
|
@@ -137,6 +137,9 @@ function foldRetryable(ctx, lane) {
|
|
|
137
137
|
// spend real tokens that no per-run total ever sees (they did, until 2026-07-28).
|
|
138
138
|
//
|
|
139
139
|
// …and the BILLING PATH rides with them — see jxBillingStamp in jx-lanes.mjs.
|
|
140
|
+
//
|
|
141
|
+
// …and so does what the turn said about the model: the id it reported, or that it ran and named none —
|
|
142
|
+
// see jxModelFields in jx-lanes.mjs.
|
|
140
143
|
function ledgerRow(runDir, row) {
|
|
141
144
|
try { appendFileSync(driverDir(runDir, "jx-completions.jsonl"), JSON.stringify(row) + "\n"); } catch { /* receipts best-effort */ }
|
|
142
145
|
}
|
|
@@ -392,7 +395,7 @@ export async function runJxSerpGrid(ctx, job, opts = {}, { runLog = () => {}, no
|
|
|
392
395
|
ledgerRow(run.runDir, { ts: new Date().toISOString(), lane, mark: markName, unit: "serp-judge", executor: judgeSource,
|
|
393
396
|
...jxBillingStamp(judgeSource, jr),
|
|
394
397
|
took_ms: jr?.tookMs ?? (Date.now() - started), ok: Boolean(jr?.ok), judged: jr?.ok ? (jr.judgments?.length ?? 0) : 0,
|
|
395
|
-
...(
|
|
398
|
+
...jxModelFields(judgeSource, jr),
|
|
396
399
|
...(jr?.usage ? { usage: jr.usage } : {}), ...(jr?.ok ? {} : { cause: String(jr?.cause ?? "unknown").slice(0, 300) }) });
|
|
397
400
|
if (!jr?.ok) { judgeDegraded = String(jr?.cause ?? "unknown").slice(0, 200); break; }
|
|
398
401
|
const byId = new Map(jr.judgments.map((j) => [j.id, j]));
|
|
@@ -524,7 +527,7 @@ export async function runJxNativeread(ctx, job, opts = {}, { runLog = () => {},
|
|
|
524
527
|
ledgerRow(run.runDir, { ts: new Date().toISOString(), lane, mark: markName, unit: "nativeread", executor: source,
|
|
525
528
|
...jxBillingStamp(source, r),
|
|
526
529
|
took_ms: r?.tookMs ?? (Date.now() - started), ok: Boolean(r?.ok), items: r?.ok ? (r.items?.length ?? 0) : 0,
|
|
527
|
-
...(
|
|
530
|
+
...jxModelFields(source, r),
|
|
528
531
|
...(r?.usage ? { usage: r.usage } : {}), ...(r?.ok ? {} : { cause: String(r?.cause ?? "unknown").slice(0, 300) }) });
|
|
529
532
|
if (!r?.ok) {
|
|
530
533
|
degradeUnit(run.runDir, key, st.attempts, r?.cause ?? "unknown");
|
package/driver/jx.mjs
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { readFileSync, writeFileSync, renameSync, appendFileSync } from "node:fs";
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { driverDir } from "../shared/driver-dir.mjs"; //
|
|
17
|
-
import { decideJxLanes, candidateRefusal, canonicalTerm, romanizationSpellings, LANGUAGE_LANES, jxBillingStamp } from "./jx-lanes.mjs";
|
|
17
|
+
import { decideJxLanes, candidateRefusal, canonicalTerm, romanizationSpellings, LANGUAGE_LANES, jxBillingStamp, jxModelFields } from "./jx-lanes.mjs";
|
|
18
18
|
import { cnipaSubgroupsForClasses, cnipaEditionLabel } from "./jx-subclass.mjs"; // — replaces the hand-written seed table
|
|
19
19
|
import { kebab } from "./search-policy.mjs";
|
|
20
20
|
import { JX_PROVIDERS } from "./driver.config.mjs";
|
|
@@ -512,7 +512,9 @@ export async function runJxCandidateFold(ctx, job, opts = {}, { runLog = () => {
|
|
|
512
512
|
const row = { ts: new Date().toISOString(), lane, mark: markName, executor: source, ...jxBillingStamp(source, r),
|
|
513
513
|
took_ms: r?.tookMs ?? (Date.now() - started), ok: Boolean(r?.ok),
|
|
514
514
|
candidates: r?.ok ? (r.candidates?.length ?? 0) : 0,
|
|
515
|
-
|
|
515
|
+
// the model the turn reported, as `model` and `modelActual`, or `modelActual: null` for a turn
|
|
516
|
+
// that ran and named none — see jxModelFields.
|
|
517
|
+
...jxModelFields(source, r),
|
|
516
518
|
...(r?.usage ? { usage: r.usage } : {}), ...(r?.ok ? {} : { cause: String(r?.cause ?? "unknown").slice(0, 300) }) };
|
|
517
519
|
try { appendFileSync(ledgerPath, JSON.stringify(row) + "\n"); } catch { /* receipts best-effort */ }
|
|
518
520
|
// Before the degraded-lane `continue` below: a lane that FAILED still ran a turn, and the model
|