clearotron 0.3.2 → 0.3.3-beta.1
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/CONTRIBUTING.md +12 -0
- package/INSTALL.md +8 -0
- package/bin/onboard.mjs +109 -13
- package/bin/start.mjs +1 -1
- package/build-info.json +2 -2
- package/demo/MANIFEST.json +27 -0
- package/docs/INTAKE.md +8 -0
- package/docs/architecture/04-configuration-reference.md +29 -11
- package/driver/CHANGELOG.md +49 -0
- package/driver/citation-census.json +3 -3
- package/driver/clearance-variants-record.mjs +12 -1
- package/driver/common-law-coverage-status.mjs +113 -0
- package/driver/config-inventory.mjs +1 -1
- package/driver/contract-audit.mjs +1 -1
- package/driver/contract-e3-backlog.mjs +37 -37
- package/driver/contract-vocabulary.mjs +8 -8
- package/driver/coverage-form-io.mjs +3 -1
- package/driver/coverage-form.mjs +38 -11
- package/driver/coverage-ledger.mjs +37 -7
- package/driver/coverage-union.mjs +2 -2
- package/driver/crowd-context.mjs +19 -6
- package/driver/dev-portal.mjs +3 -3
- package/driver/drainer-identity.mjs +1 -1
- package/driver/driver.config.mjs +80 -9
- package/driver/engine/CONTRACT.md +3 -2
- package/driver/engine/anthropic-agent.mjs +34 -7
- package/driver/engine/mcp/clarivate-server.mjs +4 -2
- package/driver/engine/mcp/corsearch-server.mjs +3 -1
- package/driver/engine/mcp/coverage-server.mjs +1 -1
- package/driver/engine/mcp/dispositions-server.mjs +47 -5
- package/driver/engine/mcp/euipo-server.mjs +2 -0
- package/driver/engine/mcp/free-tier-server.mjs +2 -0
- package/driver/engine/mcp/gather-config.mjs +8 -2
- package/driver/engine/mcp/probe-server.mjs +37 -0
- package/driver/engine/mcp/proposal-fields.mjs +45 -0
- package/driver/engine/mcp/recording-server.mjs +30 -0
- package/driver/engine/mcp/signa-server.mjs +2 -0
- package/driver/engine/mcp/supplemental.mjs +89 -12
- package/driver/engine/mcp/unit-note-server.mjs +50 -0
- package/driver/engine/mcp/uspto-local-server.mjs +2 -0
- package/driver/engine/openai-agent.mjs +7 -0
- package/driver/engine/probe.mjs +67 -14
- package/driver/engine/tool-refusal.mjs +16 -0
- package/driver/enqueue-schema.mjs +2 -2
- package/driver/envelope-settle.mjs +82 -13
- package/driver/findings-model.mjs +4 -4
- package/driver/gateway.mjs +18 -2
- package/driver/manager-groups-verdict.mjs +1 -1
- package/driver/matter-frame-record.mjs +24 -7
- package/driver/named-band.mjs +1 -1
- package/driver/package.json +1 -1
- package/driver/partial-payload-baseline.json +12 -3
- package/driver/pipeline-knockout.mjs +3 -3
- package/driver/pipeline.mjs +154 -50
- package/driver/plan-run-agreement-verdict.mjs +49 -0
- package/driver/portal-service.mjs +8 -4
- package/driver/progress.mjs +14 -3
- package/driver/publish/index.mjs +41 -26
- package/driver/publish/report-data.mjs +4 -3
- package/driver/publish/xlsx.mjs +26 -4
- package/driver/queue-markers.mjs +44 -0
- package/driver/queue-watch-verdict.mjs +2 -2
- package/driver/reference-score.mjs +10 -2
- package/driver/register-availability.mjs +2 -2
- package/driver/register-plan.mjs +313 -21
- package/driver/roster-verdict.mjs +1 -1
- package/driver/runner.mjs +26 -2
- package/driver/settle-stamp.mjs +10 -3
- package/driver/skills/clearance-common-law/SKILL.md +2 -0
- package/driver/skills/clearance-register/SKILL.md +44 -3
- package/driver/skills/clearance-register/digest.md +5 -5
- package/driver/skills/clearance-register/providers/clarivate.md +1 -1
- package/driver/skills/clearance-register/unit.md +39 -0
- package/driver/skills/clearance-variants/SKILL.md +1 -1
- package/driver/skills/matter-frame/SKILL.md +4 -2
- package/driver/stages.mjs +12 -5
- package/driver/status-snapshot.mjs +2 -2
- package/driver/suite-census.json +293 -29
- package/driver/synthesis-record.mjs +80 -2
- package/driver/unit-file-drift.mjs +3 -3
- package/driver/unit-inventory.mjs +2 -2
- package/driver/unit-state-verdict.mjs +1 -1
- package/driver/updater-identity.mjs +2 -3
- package/driver/variant-manifest-model.mjs +11 -1
- package/driver/verify.mjs +5 -5
- package/driver/withheld-families.mjs +104 -0
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/lib/brief.mjs +16 -12
- package/mcp-server/lib/runs.mjs +1 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +3 -2
- package/package.json +2 -2
- package/portal-ui/dist/assets/{index-DMthc7PQ.js → index-GBbbyQxc.js} +22 -4
- package/portal-ui/dist/index.html +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/_shared/count.mjs +2 -2
- package/providers/_shared/enumerate.mjs +15 -2
- package/providers/_shared/execute-plan.mjs +19 -1
- package/providers/_shared/plan-guards.mjs +40 -0
- package/providers/clarivate/src/capabilities.js +15 -5
- package/providers/clarivate/src/core.js +41 -5
- package/providers/corsearch/src/capabilities.js +4 -0
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/signa/src/capabilities.js +22 -8
- package/providers/signa/src/core.js +12 -1
- package/scripts/demo-evidence.mjs +114 -0
- package/scripts/e2e.mjs +1 -1
- package/scripts/engine-probe.mjs +6 -5
- package/scripts/env-audit.mjs +1 -1
- package/scripts/freeze-example-run.mjs +3 -3
- package/scripts/live-surface-check.mjs +32 -33
- package/scripts/mint-suite-census.mjs +66 -0
- package/scripts/package-size-budget.mjs +117 -0
- package/scripts/register-plan-shape.mjs +259 -0
- package/scripts/release-note-required.mjs +38 -1
- package/scripts/repo-writes.mjs +1 -1
- package/scripts/report-sections-render-check.mjs +7 -3
- package/scripts/score.mjs +7 -1
- package/scripts/settings-render-check.mjs +36 -0
- package/scripts/travelling-predicates.mjs +1 -1
- package/shared/identifier-scan.mjs +22 -5
- package/shared/scroll-settle.mjs +67 -0
|
@@ -497,6 +497,19 @@ serve({
|
|
|
497
497
|
},
|
|
498
498
|
watchlist_owners: { type: "array", items: { type: "string" },
|
|
499
499
|
description: "Real register owners the plan compiles owner lanes from — never sectors or descriptions." },
|
|
500
|
+
goods_words: { type: "array", items: { type: "string" },
|
|
501
|
+
description:
|
|
502
|
+
"The words the register search is narrowed to: the client's own goods and services wording " +
|
|
503
|
+
"first, then the words other filings use for the same goods that it does not already " +
|
|
504
|
+
"contain. Single words or short phrases as a specification would write them, no wildcards, " +
|
|
505
|
+
"at most 24. Each is matched against the goods and services description of registered " +
|
|
506
|
+
"marks, so use words a specification would contain; a word broader than the goods widens " +
|
|
507
|
+
"the search instead of narrowing it. The register cannot read the words and, or, not, adj " +
|
|
508
|
+
"or near inside an item — an item containing one is searched without it. Send an EMPTY " +
|
|
509
|
+
"LIST when you have considered the goods and no word is worth narrowing by; omit the " +
|
|
510
|
+
"field only when the matter states no goods at all. Those are different answers and the " +
|
|
511
|
+
"run records which one you gave.",
|
|
512
|
+
},
|
|
500
513
|
scope_ledger: {
|
|
501
514
|
type: "array",
|
|
502
515
|
description:
|
|
@@ -826,6 +839,23 @@ serve({
|
|
|
826
839
|
},
|
|
827
840
|
},
|
|
828
841
|
},
|
|
842
|
+
// THE SAME DEFECT TWICE MORE, and the same fix. The dispatch asks for both of these in its own
|
|
843
|
+
// imperative, the acceptor validates and records them, and the plan compile reads the classes —
|
|
844
|
+
// and until now neither was a property here, so a frame following its schema could send neither.
|
|
845
|
+
// A real run's frame carried no added class although the goods reached one. Shape only: the
|
|
846
|
+
// dispatch already says what to send and why, so no sentence the model reads is added here.
|
|
847
|
+
identified_classes: {
|
|
848
|
+
type: "array",
|
|
849
|
+
items: {
|
|
850
|
+
type: "object",
|
|
851
|
+
required: ["class", "reason"],
|
|
852
|
+
properties: {
|
|
853
|
+
class: { type: "integer", minimum: 1, maximum: 45 },
|
|
854
|
+
reason: { type: "string" },
|
|
855
|
+
},
|
|
856
|
+
},
|
|
857
|
+
},
|
|
858
|
+
ratified_forms: { type: "array", items: { type: "string" } },
|
|
829
859
|
// OFFERED HERE, OR NEVER SENT. The acceptor took this field for a beta and the plan acted on it,
|
|
830
860
|
// and no frame ever proposed one, because the schema a model is given did not offer it. Worded as
|
|
831
861
|
// the owner approved it; it is model-facing prose, so its wording is his.
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { serve } from "./stdio-server.mjs";
|
|
12
12
|
import { doSearch, doRecordFetch, doEnumerate, doExecutePlan, DEFAULT_BASE } from "../../../providers/signa/src/core.js";
|
|
13
13
|
import { proposeSupplemental } from "./supplemental.mjs";
|
|
14
|
+
import { narrowingFields } from "./proposal-fields.mjs";
|
|
14
15
|
import { CAPABILITIES } from "../../../providers/signa/src/capabilities.js";
|
|
15
16
|
|
|
16
17
|
const API_KEY = process.env.SIGNA_API_KEY || "";
|
|
@@ -101,6 +102,7 @@ serve({
|
|
|
101
102
|
romanization: { type: "string", description: "The Latin-script form of a NON-LATIN term. On THIS source it is NOT used to rescue the slice — nativeScriptIndex is true, so the characters are sent as themselves and the romanisation is carried for the reader only." },
|
|
102
103
|
owner: { type: "string", description: "OPTIONAL owner scope field on a MARK-TEXT proposal: the owner×term intersection, served by filters.owner_name in the same request. Not allowed on predicate:owner (there the owner name IS the term)." },
|
|
103
104
|
nice_classes: { type: "array", items: {} },
|
|
105
|
+
...narrowingFields(), // the narrowing fields every register serves (proposal-fields.mjs)
|
|
104
106
|
rationale: { type: "string" },
|
|
105
107
|
term_literal: { type: "boolean", description: "TRUE only when the term genuinely IS the mark verbatim (a multi-word slogan mark, a mark carrying an anchored star) — it bypasses the term-shape lint. Never use it to push a label through." },
|
|
106
108
|
} } },
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync } from "node:fs";
|
|
31
31
|
import { dirname, join } from "node:path";
|
|
32
32
|
import { driverDir } from "../../../shared/driver-dir.mjs"; //
|
|
33
|
-
import { PLAN_PREDICATES, PLAN_MAX_OR_WIDTH, PLAN_MAX_NAME_LENGTH, fingerprint, ownerIntersectionGap, resolveRegions, houseElementOf, withoutHouseElementTerms } from "../../register-plan.mjs";
|
|
33
|
+
import { PLAN_PREDICATES, PLAN_MAX_OR_WIDTH, PLAN_MAX_NAME_LENGTH, fingerprint, ownerIntersectionGap, goodsTextGap, resolveRegions, houseElementOf, withoutHouseElementTerms, containsFormSubstitution } from "../../register-plan.mjs";
|
|
34
34
|
import { entryTermIssues } from "../../../providers/_shared/term-shape.mjs";
|
|
35
35
|
import { isNonLatinTerm, romanizationRefusal, romanizationSpellings, nativeScriptIndexGap } from "../../../providers/_shared/script-form.mjs";
|
|
36
36
|
|
|
@@ -64,13 +64,37 @@ export function mintSupplementalEntries(axis, proposals, { existingQids = new Se
|
|
|
64
64
|
// first, non-priority ones keep their relative order behind them. The cap VALUES are unchanged
|
|
65
65
|
// (no count threshold moves), and with no priorityClasses the order is byte-identical to before.
|
|
66
66
|
const prio = new Set((priorityClasses ?? []).map((c) => String(c).trim()).filter(Boolean));
|
|
67
|
-
|
|
67
|
+
// ONE QUESTION PER GOODS WORD where the register cannot offer alternatives in one goods clause — the
|
|
68
|
+
// compiler's own rule for such a register (register-plan.mjs, goodsTextListOr). Joined, the words would
|
|
69
|
+
// be intersected and the answer would narrow as the list grew; the connector refuses that shape
|
|
70
|
+
// outright. Each word keeps the proposal's index, so a refusal still names the proposal it came from.
|
|
71
|
+
const perWord = capabilities?.goodsTextSearch === true && capabilities?.goodsTextListOr === false;
|
|
72
|
+
// ONE QUESTION PER NAME where the register has no OR at all (maxOrWidth 1) — the compiler's own width
|
|
73
|
+
// for such a register. A batch of names went out as windows of one name each under ONE question, and
|
|
74
|
+
// the enumerate kernel stopped the whole batch at the first name that crowded or failed, so every name
|
|
75
|
+
// after it was never sent. Split, each name is its own question: its own count, its own crowd decision
|
|
76
|
+
// and its own receipt row. The names of one batch still spend ONE slot of the per-call and per-axis
|
|
77
|
+
// caps between them, as the batch did; `split_of` names the batch so the next call counts it once.
|
|
78
|
+
const perName = Number(capabilities?.maxOrWidth) === 1;
|
|
79
|
+
const indexed = (proposals ?? []).flatMap((p, i) => {
|
|
80
|
+
const names = perName && Array.isArray(p?.terms) ? p.terms.map((t) => String(t ?? "").trim()).filter(Boolean) : [];
|
|
81
|
+
const splitOf = names.length > 1 && names.length <= PLAN_MAX_OR_WIDTH
|
|
82
|
+
? fingerprint({ axis, predicate: String(p?.predicate ?? "default"), names: [...names].sort(), i }) : null;
|
|
83
|
+
const byName = splitOf ? names.map((t) => { const { terms: _batch, ...rest } = p; return { ...rest, term: t }; }) : [p ?? {}];
|
|
84
|
+
return byName.flatMap((q) => {
|
|
85
|
+
const words = perWord && Array.isArray(q?.goods_words) ? q.goods_words.map((w) => String(w ?? "").trim()).filter(Boolean) : [];
|
|
86
|
+
return words.length > 1
|
|
87
|
+
? words.map((w, k) => ({ p: { ...q, goods_words: [w] }, i, slot: `${i}:${k}`, splitOf }))
|
|
88
|
+
: [{ p: q, i, slot: `${i}:0`, splitOf }];
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
const slots = new Set();
|
|
68
92
|
const ordered = prio.size
|
|
69
93
|
? [...indexed.filter(({ p }) => inPriority(p, prio)), ...indexed.filter(({ p }) => !inPriority(p, prio))]
|
|
70
94
|
: indexed;
|
|
71
|
-
for (const { p, i } of ordered) {
|
|
95
|
+
for (const { p, i, slot, splitOf } of ordered) {
|
|
72
96
|
const issue = (msg) => rejected.push({ index: i, issue: msg, proposal: compactProposal(p) });
|
|
73
|
-
if (
|
|
97
|
+
if (!slots.has(slot) && slots.size >= perCall) { issue(`per-call cap ${perCall} reached`); continue; }
|
|
74
98
|
const predicate = String(p.predicate ?? "default");
|
|
75
99
|
if (!PLAN_PREDICATES.includes(predicate)) { issue(`unknown predicate "${predicate.slice(0, 20)}" (one of: ${PLAN_PREDICATES.join(", ")})`); continue; }
|
|
76
100
|
const terms = Array.isArray(p.terms) ? p.terms.map((t) => String(t ?? "").trim()).filter(Boolean) : null;
|
|
@@ -107,6 +131,15 @@ export function mintSupplementalEntries(axis, proposals, { existingQids = new Se
|
|
|
107
131
|
if (!isNonLatinTerm(term)) { issue(`a romanization belongs ONLY on a non-Latin term — "${term.slice(0, 40)}" is already Latin script, so this romanization transliterates a DIFFERENT string; drop it or fix the term`); continue; }
|
|
108
132
|
romanizedTerms = romanizationSpellings(romanRaw);
|
|
109
133
|
}
|
|
134
|
+
// THE GOODS NARROWING, validated where every other field is. Whole words or short phrases as a
|
|
135
|
+
// specification writes them; a wildcard is a hard refusal on this field at the register, and an
|
|
136
|
+
// operator word inside an item is stripped downstream by the shared reader, so nothing here needs
|
|
137
|
+
// to invent a second rule for either.
|
|
138
|
+
const goodsWords = (Array.isArray(p.goods_words) ? p.goods_words : (typeof p.goods_words === "string" ? [p.goods_words] : []))
|
|
139
|
+
.map((w) => String(w ?? "").trim()).filter(Boolean);
|
|
140
|
+
if (p.goods_words != null && !goodsWords.length) { issue("goods_words, when present, must carry at least one word"); continue; }
|
|
141
|
+
if (goodsWords.some((w) => /[*?]/.test(w))) { issue("a goods word carries a wildcard, which this register refuses on that field"); continue; }
|
|
142
|
+
if (goodsWords.length && predicate === "owner") { issue("goods_words narrows a MARK-TEXT question; on predicate:owner the owner name is the term"); continue; }
|
|
110
143
|
const term_literal = p.term_literal === true;
|
|
111
144
|
// A1 — the same term-shape/term-predicate lint the plan freeze enforces, at the PROPOSAL seam:
|
|
112
145
|
// the model gets the reason IN-TURN (rejected[]) and can re-propose the mark-shaped term — a
|
|
@@ -205,6 +238,19 @@ export function mintSupplementalEntries(axis, proposals, { existingQids = new Se
|
|
|
205
238
|
for (const r of covered) regions.push(r);
|
|
206
239
|
}
|
|
207
240
|
}
|
|
241
|
+
// A CONTAINS-FORM PROPOSAL ON A TERM SHORTER THAN THE REGISTER'S FLOOR is asked on the exact form,
|
|
242
|
+
// the rule the compiler applies to its own goods-narrowed questions and saturation probes: the
|
|
243
|
+
// register refuses the contains form for a term that short, so the question as proposed would come
|
|
244
|
+
// back as an error rather than an answer. Same classes, goods words and scope, one entry for one,
|
|
245
|
+
// and the entry says so. Decided BEFORE the fingerprint, so the qid names the question actually
|
|
246
|
+
// asked. A stack is switched only when every member is below the floor; a mixed stack keeps the
|
|
247
|
+
// form the model chose, and its short members are refused and disclosed on their own.
|
|
248
|
+
let askedPredicate = predicate;
|
|
249
|
+
let substituted = null;
|
|
250
|
+
if (predicate === "default") {
|
|
251
|
+
const subs = (terms ?? [term]).map((t) => containsFormSubstitution(t, capabilities));
|
|
252
|
+
if (subs.length && subs.every(Boolean)) { askedPredicate = "exact"; substituted = subs[0]; }
|
|
253
|
+
}
|
|
208
254
|
const anchor = terms ? terms[0] : term;
|
|
209
255
|
// The fingerprint (⇒ the qid) deliberately EXCLUDES the romanization: the qid names the QUESTION
|
|
210
256
|
// (which term, which predicate, which scope) and the romanisation is carriage, not a different
|
|
@@ -212,37 +258,68 @@ export function mintSupplementalEntries(axis, proposals, { existingQids = new Se
|
|
|
212
258
|
// with the Latin form added — the exact wedge shape the regions inheritance above exists to kill.
|
|
213
259
|
// Instead a re-proposal that adds a romanisation to a stored bare qid ENRICHES it (below), the
|
|
214
260
|
// same field-level, never-term-changing merge extendRegisterPlan applies to the dictated plan.
|
|
215
|
-
|
|
216
|
-
|
|
261
|
+
// …and it deliberately INCLUDES the goods narrowing, for the opposite reason. A goods-limited
|
|
262
|
+
// re-ask of a crowded question is the same term, predicate, classes and scope — it differs only by
|
|
263
|
+
// what the filings must cover. Excluded, it would mint the crowd's own qid and be read as a
|
|
264
|
+
// re-proposal of the question it exists to replace, so the first move against a crowd would
|
|
265
|
+
// silently become no move at all.
|
|
266
|
+
const fp = String(fingerprint({ predicate: askedPredicate, term: term || null, terms: terms || null, nice_classes: nice, regions,
|
|
267
|
+
...(owner ? { owner } : {}), ...(goodsWords.length ? { goods: [...goodsWords].sort() } : {}) })).replace(/^fnv1a:/, "");
|
|
268
|
+
const qid = `supp:${axis}:${askedPredicate}:${slug(anchor)}:${fp.slice(0, 8)}`;
|
|
217
269
|
if (existingQids.has(qid) || minted.some((e) => e.qid === qid)) {
|
|
218
270
|
reused.push(qid);
|
|
219
271
|
if (romanizedTerms && existingQids.has(qid)) enriched.push({ qid, term, romanizedTerms });
|
|
220
272
|
continue;
|
|
221
273
|
}
|
|
222
|
-
if (
|
|
223
|
-
|
|
274
|
+
if (!slots.has(slot)) {
|
|
275
|
+
if (budget <= 0) { issue(`per-axis cap ${axisMax} reached — assess whether an existing supplemental already covers this`); continue; }
|
|
276
|
+
budget -= 1;
|
|
277
|
+
}
|
|
278
|
+
slots.add(slot);
|
|
224
279
|
const entry = {
|
|
225
|
-
qid, axis, predicate,
|
|
280
|
+
qid, axis, predicate: askedPredicate,
|
|
281
|
+
...(substituted ? { contains_substituted: substituted } : {}),
|
|
226
282
|
...(terms ? { terms } : { term }),
|
|
227
283
|
...(romanizedTerms ? { romanizedTerms } : {}),
|
|
228
284
|
...(owner ? { owner } : {}),
|
|
285
|
+
// Carried as `goods_text`, the field the plan and every connector already speak — the tool calls
|
|
286
|
+
// it `goods_words` because that is what the manual and the manifest call it to the model.
|
|
287
|
+
...(goodsWords.length ? { goods_text: goodsWords } : {}),
|
|
288
|
+
// What this question REPLACED. The ledger shows the crowd and its narrowing together, each with
|
|
289
|
+
// its own count, so a crowd never reads as a question nobody answered.
|
|
290
|
+
...(typeof p.narrows === "string" && p.narrows.trim() ? { narrows: p.narrows.trim().slice(0, 200) } : {}),
|
|
229
291
|
...(term_literal ? { term_literal: true } : {}),
|
|
230
292
|
nice_classes: nice, regions,
|
|
231
293
|
expected_kind: "enumerate",
|
|
232
294
|
origin: "supplemental",
|
|
295
|
+
...(splitOf ? { split_of: splitOf } : {}),
|
|
233
296
|
...(typeof p.rationale === "string" && p.rationale.trim() ? { rationale: p.rationale.trim().slice(0, 200) } : {}),
|
|
234
297
|
};
|
|
235
298
|
// F1 — an owner×term slice on a provider that cannot intersect them is minted as an UNSUPPORTED
|
|
236
299
|
// entry (→ the executor's deferred lane → a disclosed coverage row), exactly like a missing
|
|
237
300
|
// predicate at compile time. Never rejected (the gap belongs on the record) and never silently
|
|
238
301
|
// widened into an owner-less sweep.
|
|
239
|
-
|
|
240
|
-
|
|
302
|
+
// …and a goods narrowing on a register that cannot search goods text is the same kind of gap, with
|
|
303
|
+
// the compiler's own reason (goodsTextGap): recorded, never run on the class alone, which would ask the
|
|
304
|
+
// crowd the narrowing exists to cut.
|
|
305
|
+
const gap = ownerIntersectionGap(entry, capabilities) ?? goodsTextGap(entry, capabilities);
|
|
306
|
+
if (gap) { entry.unsupported = true; entry.unsupported_reason = gap; }
|
|
241
307
|
minted.push(entry);
|
|
242
308
|
}
|
|
243
309
|
return { minted, reused, rejected, enriched, narrowed };
|
|
244
310
|
}
|
|
245
311
|
|
|
312
|
+
/** The per-axis cap's count of what is already on file: the names of one split batch are one slot. PURE. */
|
|
313
|
+
export function supplementalSlots(entries) {
|
|
314
|
+
const batches = new Set();
|
|
315
|
+
let n = 0;
|
|
316
|
+
for (const e of (entries ?? [])) {
|
|
317
|
+
if (e?.split_of) { if (!batches.has(e.split_of)) { batches.add(e.split_of); n += 1; } }
|
|
318
|
+
else n += 1;
|
|
319
|
+
}
|
|
320
|
+
return n;
|
|
321
|
+
}
|
|
322
|
+
|
|
246
323
|
/**
|
|
247
324
|
* Append mint-rejection rows to a supplemental-plan doc's `rejected[]` (append-only, beside
|
|
248
325
|
* entries[] — one sidecar per axis holds BOTH what folded into the plan and what died at the seam,
|
|
@@ -326,7 +403,7 @@ export async function proposeSupplemental(params, tctx, deps) {
|
|
|
326
403
|
return { type: "text", text: JSON.stringify({ minted: [], reused: [], rejected: [], excluded_house_element: excludedHouse, executed: false }, null, 2) };
|
|
327
404
|
const existingQids = new Set(supp.entries.map((e) => e.qid));
|
|
328
405
|
const { minted, reused, rejected, enriched, narrowed } = mintSupplementalEntries(axis, offered,
|
|
329
|
-
{ existingQids, perCall, axisMax, existingCount: supp.entries
|
|
406
|
+
{ existingQids, perCall, axisMax, existingCount: supplementalSlots(supp.entries), capabilities: deps.capabilities ?? null, priorityClasses: planClasses });
|
|
330
407
|
|
|
331
408
|
// Field-level romanisation enrichment of a REUSED qid (2026-07-30 review round): the natural retry —
|
|
332
409
|
// a bare non-Latin proposal deferred at the wire, the model re-proposes it WITH the romanisation —
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
// for it and this tool never guesses one.
|
|
41
41
|
import { serve } from "./stdio-server.mjs";
|
|
42
42
|
import { recordUnitNote } from "../../register-unit-record.mjs";
|
|
43
|
+
import { recordWithheldFamilies, WITHHELD_REASON_MAX } from "../../withheld-families.mjs";
|
|
43
44
|
|
|
44
45
|
async function record_unit_note(params) {
|
|
45
46
|
const runDir = String(process.env.CLEAROTRON_BAND_RUN_DIR ?? "");
|
|
@@ -66,6 +67,38 @@ async function record_unit_note(params) {
|
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
// ── THE FAMILIES THE READING TURN LEAVES UNASKED (withheld-families.mjs) ───────────────────────────
|
|
71
|
+
//
|
|
72
|
+
// Same server, same binding: the axis is the one the driver fanned this seat out for, and a payload
|
|
73
|
+
// naming another is refused. What it records is a judgment only this turn can make — which waiting
|
|
74
|
+
// families were not worth asking, and why — so it lives beside the turn's note rather than on the
|
|
75
|
+
// digest's coverage tool, which never saw the families.
|
|
76
|
+
async function record_withheld_families(params) {
|
|
77
|
+
const runDir = String(process.env.CLEAROTRON_BAND_RUN_DIR ?? "");
|
|
78
|
+
if (!runDir) return { isError: true, text: "ERROR: this server was started without a run — the driver wires it per run; there is no parameter for it and this tool never guesses one." };
|
|
79
|
+
const bound = String(process.env.CLEAROTRON_RECORD_AXIS ?? "").trim();
|
|
80
|
+
if (!bound) return { isError: true, text: "ERROR: this server was started without a bound axis — the driver binds the axis it fanned out for." };
|
|
81
|
+
const named = String(params?.axis ?? "").trim();
|
|
82
|
+
if (named && named !== bound)
|
|
83
|
+
return { isError: true, text: `ERROR: unit_axis_not_yours:${named} — you are the seat for axis "${bound}". Send "${bound}" or omit the field.` };
|
|
84
|
+
try {
|
|
85
|
+
const r = recordWithheldFamilies(runDir, { axis: bound, families: params?.families });
|
|
86
|
+
if (r.refused) return { isError: true, text: `REFUSED: ${r.refused}` };
|
|
87
|
+
if (r.write_failed) return { isError: true, text: `ERROR: the driver could not store this record (${r.write_failed}). This is a driver fault — do not re-type it.` };
|
|
88
|
+
const refused = r.rejected.map((x) => `- ${x.qid || "(no qid)"}: ${x.issue}`);
|
|
89
|
+
const left = r.still_to_judge;
|
|
90
|
+
return { isError: !r.recorded.length && refused.length > 0, text: [
|
|
91
|
+
`Recorded ${r.recorded.length} famil${r.recorded.length === 1 ? "y" : "ies"} as withheld-by-judgment on axis "${r.axis}".`,
|
|
92
|
+
...(refused.length ? ["Refused:", ...refused] : []),
|
|
93
|
+
left.length
|
|
94
|
+
? `Still to judge on this axis — waiting, not asked, no reason recorded (${left.length}): ${left.slice(0, 40).join(", ")}${left.length > 40 ? ", …" : ""}`
|
|
95
|
+
: "Every waiting family on this axis is now asked or recorded.",
|
|
96
|
+
].join("\n") };
|
|
97
|
+
} catch (e) {
|
|
98
|
+
return { isError: true, text: `ERROR: the driver could not record this call (${String(e?.message ?? e).slice(0, 200)}). This is a driver fault — do not re-type it.` };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
69
102
|
serve({
|
|
70
103
|
name: "unit-note", version: "0.1.0",
|
|
71
104
|
tools: [{
|
|
@@ -84,5 +117,22 @@ serve({
|
|
|
84
117
|
} },
|
|
85
118
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
86
119
|
handler: record_unit_note,
|
|
120
|
+
}, {
|
|
121
|
+
name: "record_withheld_families",
|
|
122
|
+
description:
|
|
123
|
+
"Record the WAITING families on this axis that you decided NOT to ask, each with your reason. A family you " +
|
|
124
|
+
"do not ask was never searched: it is recorded withheld-by-judgment, and the reason goes into the run's " +
|
|
125
|
+
"record and the audit workbook, never into the report. Every waiting family must end this run either asked " +
|
|
126
|
+
"or recorded here — one nobody judged holds up delivery. One reason may cover several families. The answer " +
|
|
127
|
+
"lists the waiting families on this axis that are still neither asked nor recorded.",
|
|
128
|
+
inputSchema: { type: "object", required: ["families"], properties: {
|
|
129
|
+
families: { type: "array", items: { type: "object", required: ["qids", "reason"], properties: {
|
|
130
|
+
qids: { type: "array", items: { type: "string" }, description: "The qids of waiting families on this axis, exactly as the dispatch lists them." },
|
|
131
|
+
reason: { type: "string", description: `Why these were not asked, in a lawyer's words (at most ${WITHHELD_REASON_MAX} characters). The audit workbook prints it.` },
|
|
132
|
+
} } },
|
|
133
|
+
axis: { type: "string", description: "Optional, and checked rather than trusted: the driver binds the axis it dispatched you for." },
|
|
134
|
+
} },
|
|
135
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
136
|
+
handler: record_withheld_families,
|
|
87
137
|
}],
|
|
88
138
|
});
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
CAPABILITIES, DEFAULT_DB_ENV, doSearch, doRecordFetch, doBatchScreen, doEnumerate, doExecutePlan,
|
|
37
37
|
} from "../../../providers/uspto-local/src/core.js";
|
|
38
38
|
import { proposeSupplemental } from "./supplemental.mjs";
|
|
39
|
+
import { narrowingFields } from "./proposal-fields.mjs";
|
|
39
40
|
|
|
40
41
|
const DB_PATH = process.env[DEFAULT_DB_ENV] || "";
|
|
41
42
|
// The auth object IS the index path — see the core's header. Passed as an object rather than a bare
|
|
@@ -139,6 +140,7 @@ serve({
|
|
|
139
140
|
romanization: { type: "string", description: "The Latin-script form of a NON-LATIN term — plain ASCII letters/digits, syllable-separated by single spaces, no tone marks or diacritics. On THIS source it does not rescue the slice: nativeScriptIndex is undeclared, so a non-Latin term defers and is disclosed rather than being answered by its romanisation." },
|
|
140
141
|
owner: { type: "string", description: "OPTIONAL owner scope field on a MARK-TEXT proposal: the query is the owner×term intersection (the owner's filings within the term band). Not allowed on predicate:owner (there the owner name IS the term)." },
|
|
141
142
|
nice_classes: { type: "array", items: {} },
|
|
143
|
+
...narrowingFields(), // the narrowing fields every register serves (proposal-fields.mjs)
|
|
142
144
|
rationale: { type: "string" },
|
|
143
145
|
term_literal: { type: "boolean", description: "TRUE only when the term genuinely IS the mark verbatim (a multi-word slogan mark, a mark carrying an anchored star) — it bypasses the term-shape lint. Never use it to push a label through." },
|
|
144
146
|
} } },
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
// `--dangerously-bypass-approvals-and-sandbox` — see buildCodexArgs below for why.
|
|
24
24
|
|
|
25
25
|
import { mkdtempSync, writeFileSync, copyFileSync, existsSync, rmSync, readFileSync, readdirSync, statSync, symlinkSync, lstatSync, realpathSync } from "node:fs";
|
|
26
|
+
import { everyToolCallRefused } from "./tool-refusal.mjs";
|
|
26
27
|
import { writeSecretFile } from "../../shared/secret-file.mjs"; // the rotated login goes back the way every credential is written
|
|
27
28
|
import { tmpdir, homedir } from "node:os";
|
|
28
29
|
import { join } from "node:path";
|
|
@@ -531,6 +532,12 @@ function settleTuple({ r, ev, resumeRef }) {
|
|
|
531
532
|
// The engine's sign-in could not be refreshed: what the operator runs to fix it. The gateway names the
|
|
532
533
|
// stage's failure with it.
|
|
533
534
|
signedOut: signedOut ? "codex sign-in expired — run `codex login`, then start the search again" : undefined,
|
|
535
|
+
// Every tool call refused and none completed: codex's own sandbox on this host, and what fixes it. The
|
|
536
|
+
// turn is still `ok` here; when the stage then fails, the gateway names its failure with this, as it
|
|
537
|
+
// names a sign-in, instead of the missing file the refusal left behind.
|
|
538
|
+
toolsRefused: everyToolCallRefused(mcpToolGauge(ev))
|
|
539
|
+
? "codex refused every tool call this search needs — set CLEAROTRON_CODEX_SANDBOX_BYPASS=1 in this install's environment file, or use the Anthropic engine, then start the search again"
|
|
540
|
+
: undefined,
|
|
534
541
|
rateLimitBasis: rateLimited ? "text-match" : undefined,
|
|
535
542
|
// resetsAtBasis (2026-08-20): same honesty as rateLimitBasis one line up, for the reset
|
|
536
543
|
// CLOCK rather than the classification. codex states its reset as human prose with NO timezone
|
package/driver/engine/probe.mjs
CHANGED
|
@@ -15,11 +15,20 @@
|
|
|
15
15
|
//
|
|
16
16
|
// THE CHEAPEST TURN THAT PROVES THE WHOLE PATH
|
|
17
17
|
//
|
|
18
|
-
// One `haiku`-tier turn at `low` effort
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// stage
|
|
18
|
+
// One `haiku`-tier turn at `low` effort, asked to call one tool, with no skills dir and no run dir. The
|
|
19
|
+
// tool is `ping` on engine/mcp/probe-server.mjs, handed over exactly as a stage hands over its own.
|
|
20
|
+
//
|
|
21
|
+
// WHY IT CALLS A TOOL. It used to call none, and a turn with no tools proves nothing about the tools every
|
|
22
|
+
// search stage is given. On some hosts codex's own sandbox refuses every tool call while the turn reports
|
|
23
|
+
// success; the probe passed there, and every search then failed after real spend. So the probe now passes
|
|
24
|
+
// only when the reply carries the word `ping` returned, a random word minted for this turn and given to
|
|
25
|
+
// that server alone, so the model cannot supply it. Every call refused is a configuration fault, refused at
|
|
26
|
+
// the door; no call and no word shows nothing either way, and warns.
|
|
27
|
+
//
|
|
28
|
+
// It exercises every link a stage uses: binary → spawn → billing mode → credential → model access → a tool
|
|
29
|
+
// call through the stage's own tool path → a completed turn parsed by the adapter's own settle path. And
|
|
30
|
+
// it is far lighter than the thing it protects: one register-sweep stage prompt inlines 150 KB of plan and
|
|
31
|
+
// runs for minutes.
|
|
23
32
|
//
|
|
24
33
|
// WHAT IT DOES NOT PROVE, said out loud. It exercises the CHEAP tier. Both tiers ride one credential on
|
|
25
34
|
// a subscription, so AUTH is proven for all of them; a per-tier model entitlement or a per-tier quota is
|
|
@@ -56,9 +65,28 @@
|
|
|
56
65
|
|
|
57
66
|
import { ENGINE_BINARIES, DEFAULT_ENGINE_ID, engineAdapterSpecifier, resolveEngineProgram } from "../driver.config.mjs";
|
|
58
67
|
import { resolveAuthMode, CLOUD_SETTINGS, CLOUD_CREDENTIAL_CHECK } from "./auth.mjs";
|
|
68
|
+
import { everyToolCallRefused } from "./tool-refusal.mjs";
|
|
69
|
+
import { randomBytes } from "node:crypto";
|
|
70
|
+
import { fileURLToPath } from "node:url";
|
|
71
|
+
|
|
72
|
+
/** One tool call and one word back: short enough to be free in practice, and it needs a working tool path. */
|
|
73
|
+
export const PROBE_PROMPT = "Call the ping tool once, then reply with exactly the word it returned.";
|
|
74
|
+
/** The server and tool the probe hands the engine, named as a stage names its own (`mcp__<server>__<tool>`). */
|
|
75
|
+
export const PROBE_SERVER = "probe";
|
|
76
|
+
export const PROBE_TOOL = "ping";
|
|
77
|
+
const PROBE_SERVER_PATH = fileURLToPath(new URL("./mcp/probe-server.mjs", import.meta.url));
|
|
78
|
+
|
|
79
|
+
/** The tool config for one probe turn, in the shape both adapters take from a stage. */
|
|
80
|
+
export function probeToolConfig(sentinel) {
|
|
81
|
+
return {
|
|
82
|
+
mcpConfig: JSON.stringify({ mcpServers: { [PROBE_SERVER]: {
|
|
83
|
+
command: process.execPath, args: [PROBE_SERVER_PATH, String(sentinel)], env: {} } } }),
|
|
84
|
+
allowedTools: `mcp__${PROBE_SERVER}__${PROBE_TOOL}`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
59
87
|
|
|
60
|
-
/**
|
|
61
|
-
export const
|
|
88
|
+
/** A word no model would produce unprompted, fresh per turn. */
|
|
89
|
+
export const mintProbeSentinel = () => `probe-${randomBytes(4).toString("hex")}`;
|
|
62
90
|
/** The CHEAP rung of the driver's tier vocabulary on BOTH adapters (engine/CONTRACT.md §3). */
|
|
63
91
|
export const PROBE_MODEL = "haiku";
|
|
64
92
|
/** The floor rung of both EFFORT tables. There is nothing below `low` on anthropic. */
|
|
@@ -159,7 +187,7 @@ const capitalised = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
|
159
187
|
* (`{ source, path }`); both only shape the advice, never the mode, so the run door refuses exactly what it
|
|
160
188
|
* refused before. Absent, the advice is the subscription's, naming the bare program word.
|
|
161
189
|
*/
|
|
162
|
-
export function classifyProbe({ engine, tuple = null, error = null, timeoutSec = PROBE_TIMEOUT_SEC, auth = null, program = null } = {}) {
|
|
190
|
+
export function classifyProbe({ engine, tuple = null, error = null, timeoutSec = PROBE_TIMEOUT_SEC, auth = null, program = null, expect = null } = {}) {
|
|
163
191
|
const id = String(engine ?? "").trim().toLowerCase();
|
|
164
192
|
const v = (mode, basis, headline, fix, extra = {}) =>
|
|
165
193
|
({ ok: false, engine: id, mode, basis, headline, fix, detail: null, ...extra });
|
|
@@ -197,6 +225,21 @@ export function classifyProbe({ engine, tuple = null, error = null, timeoutSec =
|
|
|
197
225
|
// which pipe this happened to look at.
|
|
198
226
|
const detail = tail(tuple.stderr) ?? tail(tuple.stdout);
|
|
199
227
|
|
|
228
|
+
// THE TOOL, ON A TURN THAT COMPLETED. `expect` is the word the probe's tool returns, and only a probe that
|
|
229
|
+
// handed the engine a tool passes one; without it a completed turn proves what it always proved. With
|
|
230
|
+
// it, three outcomes, never two. Every call refused is this machine's configuration, read off the
|
|
231
|
+
// adapter's own gauge, and the run door refuses on it. The word in the reply is the pass. Neither is a
|
|
232
|
+
// turn that shows nothing about the tools either way: not a pass, and not this box's fault to refuse on.
|
|
233
|
+
if (tuple.code === 0 && expect != null) {
|
|
234
|
+
if (everyToolCallRefused(tuple))
|
|
235
|
+
return v("tools-refused", "tool-gauge", `${id} refused every tool call it was given`, toolsRefusedFix(id),
|
|
236
|
+
{ detail: tail(tuple.mcpToolCallRefusals?.[0]?.message) });
|
|
237
|
+
if (!String(tuple.stdout ?? "").includes(String(expect)))
|
|
238
|
+
return v("tools-unproven", "no-tool-answer", `${id} did not return the word its probe tool gives`,
|
|
239
|
+
"Nothing here shows that the tools a search needs work on this machine. Run this again; if it repeats, a search is likely to fail the same way.",
|
|
240
|
+
{ detail: tail(tuple.stdout) });
|
|
241
|
+
}
|
|
242
|
+
|
|
200
243
|
// A completed turn names what served it, the model and the provider as the program reported them, and
|
|
201
244
|
// null where it named neither, so a proof says which model and whose account it proved.
|
|
202
245
|
if (tuple.code === 0) return { ok: true, engine: id, mode: "ok", basis: "completed-turn", headline: `${id} completed a turn`, fix: null, detail: null,
|
|
@@ -234,7 +277,7 @@ export function classifyProbe({ engine, tuple = null, error = null, timeoutSec =
|
|
|
234
277
|
return v("tier-unavailable", "text-match", `${id} cannot reach the model it was asked for`, tierFix(id, text), { detail });
|
|
235
278
|
|
|
236
279
|
if (tuple.killed || s.stalled || s.hardWall)
|
|
237
|
-
return v("timed-out", "watchdog", `${id} started but did not finish
|
|
280
|
+
return v("timed-out", "watchdog", `${id} started but did not finish its probe turn in ${timeoutSec}s`,
|
|
238
281
|
"The binary runs and the turn produces nothing. Run the CLI by hand once and see what it is waiting for — an unanswered login prompt and a wedged MCP server both look like this.", { detail });
|
|
239
282
|
|
|
240
283
|
// The anthropic adapter's own diagnosis: the CLI exited without emitting a single stream event, i.e.
|
|
@@ -250,6 +293,14 @@ export function classifyProbe({ engine, tuple = null, error = null, timeoutSec =
|
|
|
250
293
|
"The engine's stderr below is the whole story; a turn that starts and fails is not a configuration this check can name.", { detail });
|
|
251
294
|
}
|
|
252
295
|
|
|
296
|
+
/** What fixes a host that refuses every tool call. On codex it is its own sandbox, and the setting is named. */
|
|
297
|
+
function toolsRefusedFix(engine) {
|
|
298
|
+
if (engine === "openai-agent")
|
|
299
|
+
return "Every search stage calls tools, so no search can finish here. codex's own sandbox refuses them on this machine: "
|
|
300
|
+
+ "set CLEAROTRON_CODEX_SANDBOX_BYPASS=1 in this install's environment file, or use the Anthropic engine, then run this again.";
|
|
301
|
+
return "Every search stage calls tools, so no search can finish here. The engine's stderr below is the place to start.";
|
|
302
|
+
}
|
|
303
|
+
|
|
253
304
|
/** The one place the tier doctrine is POINTED AT rather than re-authored. */
|
|
254
305
|
function tierFix(engine, msg) {
|
|
255
306
|
if (engine === "openai-agent")
|
|
@@ -290,7 +341,7 @@ export function probeFailureText(verdict) {
|
|
|
290
341
|
// classify is by definition one the door cannot claim to understand — and the cost of being wrong runs
|
|
291
342
|
// the other way here: refusing wrongly kills a run that would have worked, proceeding wrongly costs the
|
|
292
343
|
// stages before a failure the engine was going to produce anyway.
|
|
293
|
-
const CONFIGURATION_MODES = new Set(["unknown-engine", "auth-misconfigured", "signed-out", "tier-unavailable", "cannot-spawn"]);
|
|
344
|
+
const CONFIGURATION_MODES = new Set(["unknown-engine", "auth-misconfigured", "signed-out", "tier-unavailable", "cannot-spawn", "tools-refused"]);
|
|
294
345
|
|
|
295
346
|
// …and a mode alone is not enough, because `basis` says HOW WELL the mode is known and the ladder already
|
|
296
347
|
// makes that distinction for its own reasons. `startup-class` is an INFERENCE FROM SILENCE — the CLI died
|
|
@@ -299,7 +350,8 @@ const CONFIGURATION_MODES = new Set(["unknown-engine", "auth-misconfigured", "si
|
|
|
299
350
|
// hosted runner where a hermetic PATH left `env` unable to resolve node, and the probe "correctly reported
|
|
300
351
|
// a startup-class engine death" on a machine whose engine was fine. Refusing a production run on that
|
|
301
352
|
// inference manufactures the outage it was written to prevent, so it warns instead.
|
|
302
|
-
|
|
353
|
+
// `tool-gauge` is named, not inferred: the adapter counted each refused call off the engine's own stream.
|
|
354
|
+
const NAMED_BASES = new Set(["config", "text-match", "spawn-error", "tool-gauge"]);
|
|
303
355
|
|
|
304
356
|
/**
|
|
305
357
|
* "ok" | "configuration" | "weather" — PURE, and the whole of the door's judgment.
|
|
@@ -465,8 +517,9 @@ export async function probeEngineTurn({
|
|
|
465
517
|
// the environment back whatever it does. A resolver that cannot answer leaves the bare word.
|
|
466
518
|
if (knownProgram?.path) program = { source: knownProgram.source ?? null, path: knownProgram.path };
|
|
467
519
|
else try { const r = resolveEngineProgram(id); program = r.resolved ? { source: r.source, path: r.resolved } : null; } catch { /* the bare word */ }
|
|
468
|
-
const
|
|
469
|
-
|
|
520
|
+
const sentinel = mintProbeSentinel();
|
|
521
|
+
const tuple = await turn({ message: PROBE_PROMPT, model: PROBE_MODEL, thinking: PROBE_THINKING, timeoutSec, stallSec, ...probeToolConfig(sentinel) });
|
|
522
|
+
return classifyProbe({ engine: id, tuple, timeoutSec, auth, program, expect: sentinel });
|
|
470
523
|
} catch (e) {
|
|
471
524
|
return classifyProbe({ engine: id, error: e, timeoutSec, auth, program });
|
|
472
525
|
} finally {
|
|
@@ -479,7 +532,7 @@ export async function probeEngineTurn({
|
|
|
479
532
|
*
|
|
480
533
|
* The choice, stated so nobody has to re-decide it. The register-credential precedent refuses because a
|
|
481
534
|
* run whose work is impossible must say so before it costs anything, and the reasoning transfers exactly:
|
|
482
|
-
* every one of the fourteen stages spawns the engine, so an engine that cannot complete
|
|
535
|
+
* every one of the fourteen stages spawns the engine, so an engine that cannot complete its probe turn
|
|
483
536
|
* cannot complete any of them. A warning would let the run build its directory, freeze its profile and
|
|
484
537
|
* write its status sidecar, then die at stage one leaving a resumable-looking husk and a failure wearing
|
|
485
538
|
* the shape of a model fault — which is the precise outcome preflightEngineBinary exists to prevent, and
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
|
+
// engine/tool-refusal.mjs — one definition of "this turn's every tool call was refused", for the two
|
|
4
|
+
// places that act on it: the gateway, which stops a stage's retries and names the failure, and the engine
|
|
5
|
+
// probe, which refuses a host where it happens before a search is paid for. Two copies would drift.
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Every tool call the turn made was refused, and none completed. Read off the adapter's own gauge
|
|
9
|
+
* (`mcpToolCalls` / `mcpToolCallsRefused`), which counts a refusal only when the call never reached its
|
|
10
|
+
* server — a tool that ran and errored is not one. One refused call beside a completed one is a model
|
|
11
|
+
* asking for something it may not have, not a host that refuses tools, so it does not count. An engine
|
|
12
|
+
* that keeps no gauge reports nothing, and nothing is not a refusal. Pure.
|
|
13
|
+
*/
|
|
14
|
+
export function everyToolCallRefused(turn) {
|
|
15
|
+
return Number(turn?.mcpToolCallsRefused ?? 0) > 0 && Number(turn?.mcpToolCalls ?? 0) === 0;
|
|
16
|
+
}
|
|
@@ -28,7 +28,7 @@ import { productName, productSpec, checkProductScope, checkNativeLanguage, unkno
|
|
|
28
28
|
import { partitionTerritories } from "./territory-tiers.mjs";
|
|
29
29
|
// — the sidecar field map, from the module that OWNS it. The check below asks whether the prose a
|
|
30
30
|
// manifest declared actually arrived, and a local copy of that list is how the two would drift apart.
|
|
31
|
-
import { PROSE_PARTS } from "./queue-markers.mjs";
|
|
31
|
+
import { PROSE_PARTS, goodsOf } from "./queue-markers.mjs";
|
|
32
32
|
|
|
33
33
|
// ── per-run scope limits ──────────────────────────────────────────────────────────────────────────────
|
|
34
34
|
// Caps, not policy. They exist so one malformed request cannot mint an unbounded search: every extra
|
|
@@ -422,7 +422,7 @@ export function validateJob(job, { atClaim = false } = {}) {
|
|
|
422
422
|
}
|
|
423
423
|
// §B2: classes OR a goods description — either suffices; both absent ⇒ the subject can't be scoped.
|
|
424
424
|
const hasClasses = requestNamesClasses(job);
|
|
425
|
-
const hasGoods =
|
|
425
|
+
const hasGoods = goodsOf(job) !== null; // — the spellings live in queue-markers.mjs, with the fold that keeps every reader on one answer
|
|
426
426
|
if (!hasClasses && !hasGoods) {
|
|
427
427
|
// — THE GATE HAS TO ASK WHAT THE RUN WOULD ASK, not what the request typed.
|
|
428
428
|
//
|