clearotron 0.5.0-beta.2 → 0.5.0-beta.3
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 +7 -0
- package/build-info.json +2 -2
- package/docs/architecture/04-configuration-reference.md +1 -0
- package/docs/architecture/05-config-governance.md +1 -1
- package/driver/CHANGELOG.md +11 -0
- package/driver/band-shape.mjs +4 -4
- package/driver/contract-vocabulary.mjs +3 -3
- package/driver/coverage-form-io.mjs +3 -1
- package/driver/coverage-form.mjs +21 -2
- package/driver/coverage-ledger.mjs +7 -1
- package/driver/declination-call.mjs +33 -16
- package/driver/degraded-parts.mjs +6 -0
- package/driver/engine/mcp/clarivate-server.mjs +2 -0
- package/driver/engine/mcp/corsearch-server.mjs +2 -0
- package/driver/engine/mcp/declination-server.mjs +4 -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 +15 -0
- package/driver/engine/mcp/held-answers.mjs +11 -3
- package/driver/engine/mcp/signa-server.mjs +2 -0
- package/driver/engine/mcp/supplemental.mjs +5 -2
- package/driver/engine/mcp/uspto-local-server.mjs +2 -0
- package/driver/envelope-settle.mjs +13 -3
- package/driver/gateway.mjs +31 -10
- package/driver/package.json +1 -1
- package/driver/parts-not-completed.mjs +52 -11
- package/driver/pipeline-knockout.mjs +5 -4
- package/driver/pipeline.mjs +436 -72
- package/driver/placement-decisions.mjs +4 -4
- package/driver/placement-form-io.mjs +21 -1
- package/driver/placement-form.mjs +25 -15
- package/driver/placement-union.mjs +53 -8
- package/driver/publish/index.mjs +7 -3
- package/driver/runner.mjs +4 -4
- package/driver/skills/clearance-register/digest.md +1 -1
- package/driver/skills/clearance-search/phase2-execution.md +3 -3
- package/driver/skills/clearance-search/synthesis-rules.md +3 -3
- package/driver/skills/placement-inquiry/SKILL.md +3 -3
- package/driver/stages.mjs +5 -5
- package/driver/suite-census.json +84 -12
- package/driver/verify.mjs +6 -6
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/package.json +1 -1
- package/package.json +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/_shared/README.md +1 -0
- package/providers/_shared/enumerate.mjs +4 -3
- package/providers/_shared/execute-plan.mjs +24 -7
- package/providers/_shared/send-until.mjs +25 -0
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/band-shape-probe.mjs +1 -1
- package/scripts/e2e-first-time.mjs +2 -1
- package/shared/names-in-force.mjs +1 -0
- package/shared/slices-taken-out.mjs +65 -0
- package/skills/clearotron-account/SKILL.md +1 -1
- package/skills/clearotron-ops/SKILL.md +1 -1
package/.env.example
CHANGED
|
@@ -184,6 +184,13 @@ CLEAROTRON_DATABASE=clarivate
|
|
|
184
184
|
# CLEAROTRON_GATHER_SESSION_KEY, CLEAROTRON_GATHER_SESSION_ID, CLEAROTRON_GATHER_AGENT,
|
|
185
185
|
# CLEAROTRON_REGISTER_CALL_LOG, CLEAROTRON_REGISTER_RECORD_LOG
|
|
186
186
|
#
|
|
187
|
+
# The time, in epoch milliseconds, at which a turn's register tools stop sending new questions, so the
|
|
188
|
+
# unit can write what it holds inside its own time. The driver writes it per turn into each register
|
|
189
|
+
# tool server's env, a quarter of the turn (at most five minutes) ahead of the turn's deadline. Leave it
|
|
190
|
+
# empty: a value set here would be read by the driver's own code-side executor as a fixed time.
|
|
191
|
+
# effect: tuning
|
|
192
|
+
CLEAROTRON_GATHER_SEND_UNTIL_MS=
|
|
193
|
+
#
|
|
187
194
|
# THERE IS NO RETENTION SETTING AND NO CLEANUP JOB. CLEAROTRON_REGISTER_LEDGER_MAX_BYTES,
|
|
188
195
|
# CLEAROTRON_REGISTER_LEDGER_KEEP and `npm run prune:ledger` are gone with the rotation tool, because the
|
|
189
196
|
# thing they bounded no longer grows: RECORD BODIES ARE WRITTEN INTO THE RUN'S OWN DIRECTORY
|
package/build-info.json
CHANGED
|
@@ -373,6 +373,7 @@ cannot be read as one list.
|
|
|
373
373
|
| `CLEAROTRON_DUMP_JSON` | unset | Dump each attempt's raw engine envelope to `_driver/<stage>.attempt<N>.rawjson.json`. Opt-in: any value except `0`/`off`/`false`/`no`/empty arms it. |
|
|
374
374
|
| `CLEAROTRON_DISPATCH_RECORD` | **on** | Write the verbatim message of every stage dispatch to `_driver/<stage>.attempt<N>[.repair<M>].dispatch.txt`, with `{file, sha, bytes, chars, kind}` on the attempt row. **Default ON** — `0`/`off`/`false`/`no` disarms it. Unlike `CLEAROTRON_DUMP_JSON` beside it, this is opt-OUT: the question it answers ("was the model given this?") is asked *after* the run that raised it, so a flag someone had to remember would be off on exactly the run that needed it. The files carry the company's identity verbatim and are deliberately not in the artifact table. |
|
|
375
375
|
| `CLEAROTRON_GATHER_SESSION_KEY` / `CLEAROTRON_GATHER_AGENT` / `CLEAROTRON_GATHER_SESSION_ID` | set per stage | Telemetry attribution into the provider-call ledger (set by the gather config; not operator-set). |
|
|
376
|
+
| `CLEAROTRON_GATHER_SEND_UNTIL_MS` | set per turn | When a turn's register tools stop sending new questions: a quarter of the turn, at most five minutes, ahead of its deadline (set by the gather config; not operator-set). |
|
|
376
377
|
| `CLEAROTRON_RECORD_AXIS` | set per dispatch (unset ⇒ the stage is not fanned out) | Binds one fan-out turn of a recording stage to the single member it may write. `stageOnce` suffixes a fan-out stage's label with its axis, the gather config resolves `<stage>:<axis>` back to the base stage's tool group, and this carries the axis to the recording server. A call whose payload names a different member than the turn is bound to is REFUSED, so a seat cannot write into a sibling's file — without the binding every turn of the fan-out would record over member one. Set by the driver; not operator-set. |
|
|
377
378
|
| `PORTAL_READ_MODEL` | `sonnet` | The model the portal's own compose-read turn uses. Distinct from the pipeline's tiers: this is a portal surface, not a stage. |
|
|
378
379
|
| `CLEAROTRON_ORDER_PROBE_SEED` | unset | Seed for `scripts/band-shape-probe.mjs`, so an ordering probe can be replayed. A diagnostic script's knob, not a run's. |
|
|
@@ -282,7 +282,7 @@ edited — that box runs pre-rebuild code and genuinely still sets it.
|
|
|
282
282
|
`CLEAROTRON_OUTBOX_BACKOFF_MAX_RETRIES` (5) / `CLEAROTRON_OUTBOX_GIVEUP_COOLDOWN_SEC` (3600 — note this one
|
|
283
283
|
drops BACKOFF from the prefix, which the elided spelling this row used to carry actively misled about),
|
|
284
284
|
`CLEAROTRON_OUTBOX_NOPROGRESS_MAX`, `TRADEMARK_MSGID_DOMAIN` (enqueue.local).
|
|
285
|
-
Code-set per stage (not operator-set): `CLEAROTRON_GATHER_AGENT` / `CLEAROTRON_GATHER_SESSION_KEY` / `CLEAROTRON_GATHER_SESSION_ID`.
|
|
285
|
+
Code-set per stage (not operator-set): `CLEAROTRON_GATHER_AGENT` / `CLEAROTRON_GATHER_SESSION_KEY` / `CLEAROTRON_GATHER_SESSION_ID`; per turn: `CLEAROTRON_GATHER_SEND_UNTIL_MS`.
|
|
286
286
|
|
|
287
287
|
**Who the completion notice reaches** — `CLEAROTRON_REQUESTER_WHATSAPP` (empty; effect: deployment) is a
|
|
288
288
|
JSON map of requester email or handle → number, and `CLEAROTRON_WHATSAPP_OPERATOR_COPY` (on unless `0`,
|
package/driver/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# clearotron-driver
|
|
2
2
|
|
|
3
|
+
## 0.5.0-beta.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Fixed: The delivery email for a result the review could not sign off now lists the report's "Only you can close these" items.
|
|
8
|
+
- Fixed: When a clearance fails, its chat notice now goes to the person who asked for it, with a copy to the operator.
|
|
9
|
+
- Fixed: A clearance no longer repeats a finished step because a web lookup inside it timed out.
|
|
10
|
+
- Fixed: A clearance whose register search cannot finish, such as one that keeps timing out, now delivers its report instead of ending with none.
|
|
11
|
+
- Fixed: A search that cannot be shown to have run is now listed as not searched, and the report is still delivered.
|
|
12
|
+
- Fixed: Reports can set aside an identical mark registered for unrelated goods, recording the reason in the audit workbook instead of listing it as a finding.
|
|
13
|
+
|
|
3
14
|
## 0.5.0-beta.2
|
|
4
15
|
|
|
5
16
|
### Patch Changes
|
package/driver/band-shape.mjs
CHANGED
|
@@ -1129,8 +1129,8 @@ export const FLOORS_BY_NAME_HERE = "Every floor here is placed or set aside by n
|
|
|
1129
1129
|
export const A_CROWDS_ONE_ROW = "Read the first page, carry forward any line a lawyer would raise, and set the rest aside in this group's one row with the count: the client is told the field is crowded.";
|
|
1130
1130
|
export const A_CROWDS_ROW_COVERS_A_READ_FLOOR = "This row covers a floor only once you have read its line and its goods are not the client's goods.";
|
|
1131
1131
|
export const AN_UNREAD_CROWDS_ONE_ROW = "Carry forward any line a lawyer would raise, and set the rest aside in this list's one row with the count, on the ground that their goods are unreadable and the field is crowded.";
|
|
1132
|
-
export const AN_UNREAD_CROWDS_FLOOR = "
|
|
1133
|
-
export const AN_UNREAD_LISTS_FLOOR = "
|
|
1132
|
+
export const AN_UNREAD_CROWDS_FLOOR = "This row never covers a floor. Place each floor here, or set it aside by name; a goods ground needs the floor's goods read in full.";
|
|
1133
|
+
export const AN_UNREAD_LISTS_FLOOR = "Place each floor here, or set it aside by name; a goods ground needs the floor's goods read in full. Any other line here may be set aside because its goods are unknown.";
|
|
1134
1134
|
// What placement does with a line of that list before its fate, by its marker (approved wording, 2026-10-19). Placement's
|
|
1135
1135
|
// alone, as its other orders are: every other stage keeps the markers as evidence and is not told of a fate.
|
|
1136
1136
|
export const AN_UNREAD_LINE_IS_CHECKED = 'A line marked "on file, not readable", "cut by the register, no goods word found", "goods held; completeness not established" or "no goods text held; completeness not established" must be checked with band_record before its fate. Read the evidence held for it, keeping its source and goods completeness clear. A line marked "no goods on file", "fetch failed, no goods on file" or "the register\'s record lists no goods" has no goods text on file. Missing goods do not establish that the goods are unrelated.';
|
|
@@ -1231,8 +1231,8 @@ function renderGroupsMd(md, groups, rows = []) {
|
|
|
1231
1231
|
if (groups.no_goods.lines) {
|
|
1232
1232
|
// The lines the goods test could not place, read by the same size rule (ruled 2026-10-12): under a thousand
|
|
1233
1233
|
// records, a fate per line by name, opened where goods are on file; at a thousand or more, the first page,
|
|
1234
|
-
// and one row on the rest whose goods are unreadable in a crowded field. A floor is
|
|
1235
|
-
//
|
|
1234
|
+
// and one row on the rest whose goods are unreadable in a crowded field. A floor is set aside on its goods only
|
|
1235
|
+
// once they are read in full, which unknown goods never are, so a floor here is placed or set aside by name.
|
|
1236
1236
|
const ng = groups.no_goods;
|
|
1237
1237
|
const crowded = ng.records >= groups.crowded_at;
|
|
1238
1238
|
const lead = tested.source === "none"
|
|
@@ -530,9 +530,9 @@ export const INNER_CODES = Object.freeze([
|
|
|
530
530
|
// ONE CODE, TWO TOKENS, and the split is by `cause` rather than by code: verify.mjs peels the
|
|
531
531
|
// `axis_invalid` cause into its own family before counting the rest. A ruling naming only
|
|
532
532
|
// `coverage_no_status` would be true of most `no_status` records and false of the ones that matter most.
|
|
533
|
-
{ code: "no_status", mints: ["driver/coverage-form.mjs:
|
|
533
|
+
{ code: "no_status", mints: ["driver/coverage-form.mjs:939"], rollsUpTo: ["coverage_no_status", "coverage_form_axis_invalid"],
|
|
534
534
|
why: "Two composites, split on the record's `cause`: coverageFormFail() in verify.mjs for cause `axis_invalid`, and the same function for the rest. NOT the `@returns` annotation of findCoverageFormViolations() in coverage-form.mjs — that is the annotation, not the mint." },
|
|
535
|
-
{ code: "engine_vocabulary", mints: ["driver/coverage-form.mjs:
|
|
535
|
+
{ code: "engine_vocabulary", mints: ["driver/coverage-form.mjs:929"], rollsUpTo: ["coverage_form_engine_vocabulary"],
|
|
536
536
|
why: "#669 — the seat wrote an engine token into the `reason` sentence that reaches the reader's page. Checked on settled rows too, because a row the seat considers finished is exactly the one whose sentence gets printed. Namespaced by coverageFormFail() in verify.mjs; the bare code names a row, never a stage." },
|
|
537
537
|
|
|
538
538
|
// ── ONE CODE, TWO MODULES ────────────────────────────────────────────────────────────────────────
|
|
@@ -544,7 +544,7 @@ export const INNER_CODES = Object.freeze([
|
|
|
544
544
|
// failure tokens" already states the rule this row records: `coverage_form_damaged`, never a bare
|
|
545
545
|
// `form_damaged`.
|
|
546
546
|
{ code: "form_damaged",
|
|
547
|
-
mints: ["driver/connotation-search.mjs:2002", "driver/connotation-search.mjs:2078", "driver/coverage-form.mjs:
|
|
547
|
+
mints: ["driver/connotation-search.mjs:2002", "driver/connotation-search.mjs:2078", "driver/coverage-form.mjs:921"],
|
|
548
548
|
rollsUpTo: ["connotation_form_damaged", "coverage_form_damaged"],
|
|
549
549
|
why: "Minted in two lanes and namespaced per lane: connotationDispositionFail() in verify.mjs for the meaning sweep, coverageFormFail() in verify.mjs for the register digest. The namespacing is what keeps them apart — see verify.mjs's block headed \"the register coverage-form failure tokens\"." },
|
|
550
550
|
]);
|
|
@@ -32,6 +32,7 @@ import { COVERAGE_STATUSES, COVERAGE_FORM_NAME, REGISTER_AXES } from "./coverage
|
|
|
32
32
|
import { coverageFormSidecarName, parseCoverageForm } from "./coverage-form.mjs";
|
|
33
33
|
import { capabilitiesFor } from "./register-capabilities.mjs";
|
|
34
34
|
import { readWithheldFamilies, splitWaitingFamilies } from "./withheld-families.mjs";
|
|
35
|
+
import { slicesTakenOutByAxis } from "../shared/slices-taken-out.mjs"; // the slices the searches record took out of the band
|
|
35
36
|
|
|
36
37
|
export { COVERAGE_FORM_NAME };
|
|
37
38
|
|
|
@@ -150,7 +151,8 @@ export function coverageFormInput(runDir) {
|
|
|
150
151
|
return { input: { skeleton: exec.skeleton, plan, bandBlocksByAxis, deferredReasons, activeAxes,
|
|
151
152
|
bandsUnreadable, orderedTerritories, capabilities, unknownAxisUnits,
|
|
152
153
|
awaiting: Array.isArray(exec.awaiting) ? exec.awaiting : [], withheld: readWithheldFamilies(runDir),
|
|
153
|
-
mainMarkets: Array.isArray(exec.main_markets) ? exec.main_markets : []
|
|
154
|
+
mainMarkets: Array.isArray(exec.main_markets) ? exec.main_markets : [],
|
|
155
|
+
takenOut: Object.fromEntries(slicesTakenOutByAxis(runDir)) }, absent: null };
|
|
154
156
|
}
|
|
155
157
|
|
|
156
158
|
/**
|
package/driver/coverage-form.mjs
CHANGED
|
@@ -353,10 +353,14 @@ export const DEFERRED_OPEN_BECAUSE = Object.freeze({
|
|
|
353
353
|
});
|
|
354
354
|
const deferredOpenBecause = (block, receiptReason) => block?.gateway_timeout === true ? DEFERRED_OPEN_BECAUSE.timeout
|
|
355
355
|
: isProviderHardErrorReason(receiptReason) ? DEFERRED_OPEN_BECAUSE.hardError : DEFERRED_OPEN_BECAUSE.capability;
|
|
356
|
+
// A SLICE THE RUN TOOK OUT OF THE BAND (the searches not completed, parts-not-completed.mjs) is open for the reason its
|
|
357
|
+
// record gives, which is the shipped line the reader gets for it (ruled 2026-10-27): the provider could express it, and
|
|
358
|
+
// what is missing is the evidence that it ran. A taken-out slice with no qid has no plan row to defer it, so it gets a
|
|
359
|
+
// row of its own, keyed on its query.
|
|
356
360
|
|
|
357
361
|
export function coverageFormRows({ skeleton = [], activeAxes = null, plan = null,
|
|
358
362
|
bandBlocksByAxis = {}, deferredReasons = {}, bandsUnreadable = [],
|
|
359
|
-
orderedTerritories = [], capabilities = null, awaiting = [], withheld = {}, mainMarkets = [] } = {}) {
|
|
363
|
+
orderedTerritories = [], capabilities = null, awaiting = [], withheld = {}, mainMarkets = [], takenOut = {} } = {}) {
|
|
360
364
|
const skel = Array.isArray(skeleton) ? skeleton.filter((s) => s && typeof s === "object") : [];
|
|
361
365
|
// WHAT WAS RETRIEVED UNDER EACH CROWD (register-plan.mjs, crowdNarrowing), worked out from the same blocks these rows
|
|
362
366
|
// are built from and carried on its row as data: a narrowed list retrieved in full (and which), none, or for the
|
|
@@ -423,6 +427,8 @@ export function coverageFormRows({ skeleton = [], activeAxes = null, plan = null
|
|
|
423
427
|
status: null, reason: null,
|
|
424
428
|
});
|
|
425
429
|
}
|
|
430
|
+
const out = Array.isArray(takenOut?.[axis]) ? takenOut[axis] : [];
|
|
431
|
+
const takenReason = new Map(out.filter((x) => x?.qid && x?.reason).map((x) => [String(x.qid), String(x.reason)]));
|
|
426
432
|
for (const qid of deferredQids) {
|
|
427
433
|
rows.push({
|
|
428
434
|
row_id: shortId("CD", `deferred:${qid}`),
|
|
@@ -433,7 +439,20 @@ export function coverageFormRows({ skeleton = [], activeAxes = null, plan = null
|
|
|
433
439
|
// It was never searched, or it was asked and never answered, and nothing in this run asks it again, so a clean
|
|
434
440
|
// claim over it is not a judgment the seat gets to make.
|
|
435
441
|
open: true,
|
|
436
|
-
open_because: deferredOpenBecause(blockByQid.get(qid), deferredReasons?.[qid]),
|
|
442
|
+
open_because: takenReason.get(qid) ?? deferredOpenBecause(blockByQid.get(qid), deferredReasons?.[qid]),
|
|
443
|
+
status: null, reason: null,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
for (const x of out) {
|
|
447
|
+
if (x?.qid || !x?.query || !x?.reason) continue;
|
|
448
|
+
rows.push({
|
|
449
|
+
row_id: shortId("CD", `deferred:${axis}:query:${x.query}`),
|
|
450
|
+
axis, kind: "deferred",
|
|
451
|
+
unit: unitLabel(axis, { term: String(x.query) }),
|
|
452
|
+
qid: null, query: String(x.query),
|
|
453
|
+
receipt_reason: String(x.reason),
|
|
454
|
+
open: true,
|
|
455
|
+
open_because: String(x.reason),
|
|
437
456
|
status: null, reason: null,
|
|
438
457
|
});
|
|
439
458
|
}
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
// `validators` at module scope, so verify importing stages would TDZ-crash whenever verify loads
|
|
24
24
|
// first; stages.mjs re-exports both names, keeping every existing import site working).
|
|
25
25
|
|
|
26
|
+
import { plainDeferralReason } from "./deferral-row.mjs";
|
|
26
27
|
export const REGISTER_AXES = ["saturation-probe", "primary-sweep", "transliteration-numeric", "incumbent-class"];
|
|
27
28
|
|
|
28
29
|
// ── — THE READER'S NAME FOR AN AXIS, EMITTED AT SOURCE ──────────────────────────────────────────
|
|
@@ -347,13 +348,18 @@ export function coverageLedgerTableRows(rows, { reasonMax = 180 } = {}) {
|
|
|
347
348
|
// confirmed-clean rows on tainted material axes move; coverage-limited/deferred rows and non-material
|
|
348
349
|
// axes (saturation-probe) are untouched; an empty taint set is a no-op. PURE; the caller pre-reads the
|
|
349
350
|
// taint verdicts (pipeline loadCoverageLedger → readRegisterTaint).
|
|
351
|
+
// The reason a tainted axis's withdrawn clean claim carries: the shipped gap sentence (deferral-row.mjs).
|
|
352
|
+
export const TAINTED_CLEAN_REASON = plainDeferralReason("unfinished");
|
|
353
|
+
|
|
350
354
|
export function applyTaintDeferred(rows, taintedAxes) {
|
|
351
355
|
const tainted = new Set((taintedAxes ?? []).map((a) => String(a).toLowerCase()));
|
|
352
356
|
if (!tainted.size) return rows ?? [];
|
|
353
357
|
const material = (a) => !NON_MATERIAL_AXES.includes(String(a ?? "").toLowerCase());
|
|
358
|
+
// The pass that claimed clean was killed before it finished reading, so the claim is withdrawn whole: the row is open,
|
|
359
|
+
// with the shipped reason a reader gets for a part not completed, and none of the claim's words.
|
|
354
360
|
return (rows ?? []).map((r) =>
|
|
355
361
|
(r && r.status === "confirmed-clean" && tainted.has(String(r.axis ?? "").toLowerCase()) && material(r.axis))
|
|
356
|
-
? { ...r, status: "deferred", reason:
|
|
362
|
+
? { ...r, status: "deferred", reason: TAINTED_CLEAN_REASON }
|
|
357
363
|
: r);
|
|
358
364
|
}
|
|
359
365
|
|
|
@@ -117,6 +117,13 @@ export const DECLINATION_REASONS = Object.freeze({
|
|
|
117
117
|
gloss: "an off-field name that is not a major brand, an active dispute or proceeding, or a "
|
|
118
118
|
+ "well-known enforcer — omitted, never listed for completeness",
|
|
119
119
|
},
|
|
120
|
+
// A COMPARISON OF MARKS (ruled 2026-10-27): the writer may set aside a near spelling on its sign, as the write-up
|
|
121
|
+
// may, with the write-up's own gloss. It is refused on the applied-for mark itself, which no comparison sets aside.
|
|
122
|
+
sign: {
|
|
123
|
+
discretionary: true,
|
|
124
|
+
rules: 'synthesis-rules.md, "The consumer confusion test — the governing gate"',
|
|
125
|
+
gloss: "a near spelling a buyer in this market could not take for the mark, by sound, by look or by meaning",
|
|
126
|
+
},
|
|
120
127
|
"not-worth-the-line": {
|
|
121
128
|
discretionary: true,
|
|
122
129
|
rules: 'synthesis-rules.md, "Promote-or-omit"',
|
|
@@ -206,7 +213,7 @@ export function contradictionFor(reason, row, scope) {
|
|
|
206
213
|
// class is closed to the entry that holds it. Each record is judged on its own mark, status and classes.
|
|
207
214
|
if (Array.isArray(row?.members) && row.members.length) {
|
|
208
215
|
for (const m of row.members) {
|
|
209
|
-
const clash = contradictionFor(reason, { mark: m?.mark ?? row.mark, status: m?.status ?? null, classes: m?.classes ?? [] }, scope);
|
|
216
|
+
const clash = contradictionFor(reason, { mark: m?.mark ?? row.mark, status: m?.status ?? null, classes: m?.classes ?? [], goods: m?.goods ?? null }, scope);
|
|
210
217
|
if (clash) return clash;
|
|
211
218
|
}
|
|
212
219
|
return "";
|
|
@@ -218,14 +225,26 @@ export function contradictionFor(reason, row, scope) {
|
|
|
218
225
|
// it switched the identical-mark refusal below off on exactly those runs.
|
|
219
226
|
const marks = Array.isArray(scope?.marks) ? scope.marks : (scope?.marks ? [scope.marks] : []);
|
|
220
227
|
|
|
228
|
+
// ── AN IDENTICAL MARK IN A FILED CLASS MAY BE DECLINED ON ITS GOODS, AND ON NOTHING ELSE DISCRETIONARY ──
|
|
229
|
+
//
|
|
230
|
+
// Ruled 2026-10-27: identical spelling and a shared class no longer bar a supported judgment that the registered
|
|
231
|
+
// goods are unrelated. This refused every discretionary token on such a record, so on a test run 29 goods
|
|
232
|
+
// decisions were refused and every one came back as a finding. A goods decision now stands where the run holds the
|
|
233
|
+
// record's goods complete (goods-evidence.mjs), because cut, unknown or missing goods establish nothing. The
|
|
234
|
+
// record's field, the report's length and a comparison with the applied-for mark itself are still refused.
|
|
221
235
|
if (isIdenticalToAppliedMark(row?.mark, marks) && isLive(row?.status) && shared.length) {
|
|
222
|
-
|
|
223
|
-
+
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
236
|
+
const facts = `this record's mark is IDENTICAL to the applied-for mark, its status is `
|
|
237
|
+
+ `${String(row?.status ?? "").trim() || "live"}, and it is in filed class ${shared.join(", ")}`;
|
|
238
|
+
if (reason === "unrelated-goods") {
|
|
239
|
+
if (row?.goods === "complete") return "";
|
|
240
|
+
return `${facts}. The run holds its goods as ${String(row?.goods ?? "").trim() || "not recorded"}, not in full, `
|
|
241
|
+
+ `and incomplete goods cannot show the goods are unrelated. Deliver and rate it, or use own-right or `
|
|
242
|
+
+ `duplicate-of-delivered if one is true.`;
|
|
243
|
+
}
|
|
244
|
+
return `${facts}. Such a mark is never declined for its field, for want of room, or on its sign. Deliver and `
|
|
245
|
+
+ `rate it; a Stage-2 mitigant goes into the rating. If its registered goods, read in full, do not meet the `
|
|
246
|
+
+ `matter's goods, decline it as unrelated-goods and say so in the ground. If it is the applicant's own right `
|
|
247
|
+
+ `or already delivered, use own-right or duplicate-of-delivered.`;
|
|
229
248
|
}
|
|
230
249
|
|
|
231
250
|
// ── ECONOMY IS NOT A REASON ABOUT A LIVE IN-CLASS MARK (ruling 2026-09-07) ─────────────────
|
|
@@ -250,14 +269,12 @@ export function contradictionFor(reason, row, scope) {
|
|
|
250
269
|
// It does effectively retire this token for live in-class records, because the sweep is already scoped
|
|
251
270
|
// to the instructed classes. That was put to the owner as the consequence and ruled anyway.
|
|
252
271
|
if (reason === "not-worth-the-line" && isLive(row?.status) && shared.length) {
|
|
253
|
-
return `this record is ${String(row?.status ?? "").trim() || "live"}
|
|
254
|
-
+
|
|
255
|
-
+ `
|
|
256
|
-
+ `
|
|
257
|
-
+ `
|
|
258
|
-
+ `if
|
|
259
|
-
+ `own-right if it belongs to the applicant, duplicate-of-delivered if the same right is already `
|
|
260
|
-
+ `reported. Say what makes this mark distant, or give it its line.`;
|
|
272
|
+
return `this record is ${String(row?.status ?? "").trim() || "live"} in instructed class ${shared.join(", ")}. `
|
|
273
|
+
+ `A live mark in an instructed class is never declined for want of room. Deliver it, or decline it on a fact `
|
|
274
|
+
+ `about this mark: unrelated-goods if its goods do not meet the matter's; off-field-not-major if it is `
|
|
275
|
+
+ `off-field and not a major brand, an active dispute or a well-known enforcer; sign if a buyer in this market `
|
|
276
|
+
+ `could not take its spelling for the mark; own-right if it belongs to the applicant; duplicate-of-delivered `
|
|
277
|
+
+ `if the same right is already reported.`;
|
|
261
278
|
}
|
|
262
279
|
|
|
263
280
|
// THERE IS NO CLASS-OVERLAP REFUSAL, AND THE FIRST DRAFT HAD ONE. It refused `unrelated-goods` and
|
|
@@ -171,6 +171,12 @@ function register(runDir, log) {
|
|
|
171
171
|
const open = readPartNotCompleted(runDir, "assessment");
|
|
172
172
|
if (open && ["placement-inquiry", "register-digest"].includes(open.stage))
|
|
173
173
|
failed.push([{ fail: `${open.stage} did not complete: ${open.reason ?? "no cause recorded"}` }, "the register write-up"]);
|
|
174
|
+
// The searches a check could not prove ran, taken out of the band and delivered as open rows (parts-not-completed.mjs).
|
|
175
|
+
const searches = readPartNotCompleted(runDir, "searches");
|
|
176
|
+
if (searches) failed.push([{ fail: [
|
|
177
|
+
(searches.slices ?? []).length ? `${searches.slices.length} slice(s) taken out of the band` : "",
|
|
178
|
+
(searches.disclosed_axes ?? []).length ? `the pass on ${searches.disclosed_axes.join(", ")} timed out, read as it stands with its clean claims open` : "",
|
|
179
|
+
].filter(Boolean).join("; ") + `: ${searches.reason ?? "no cause recorded"}` }, "the searches"]);
|
|
174
180
|
let reopen = null;
|
|
175
181
|
try { reopen = JSON.parse(readFileSync(driverDir(runDir, "frame-reopen.json"), "utf8")); } catch { reopen = null; }
|
|
176
182
|
if (reopen?.dominant_element_gap === true && reopen?.domClosed !== true)
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
// `match_mode: "phonetic"` on register_search / register_enumerate remains fully available; it is the
|
|
30
30
|
// PREVIEW of the expansion that this provider cannot give, not phonetic search itself.
|
|
31
31
|
import { serve } from "./stdio-server.mjs";
|
|
32
|
+
import { sendUntilFromEnv } from "../../../providers/_shared/send-until.mjs";
|
|
32
33
|
import { crowdNoteFor } from "./crowd-note.mjs"; // how the unit decides on a crowd, beside an answer that carries one
|
|
33
34
|
import {
|
|
34
35
|
CAPABILITIES, doSearch, doRecordFetch, doImageFetch, doBatchScreen, doEnumerate, doExecutePlan, DEFAULT_BASE,
|
|
@@ -43,6 +44,7 @@ const tctx = (kind) => ({
|
|
|
43
44
|
agentId: process.env.CLEAROTRON_GATHER_AGENT || "localagent",
|
|
44
45
|
sessionKey: process.env.CLEAROTRON_GATHER_SESSION_KEY || "", // run session key — telemetry attribution
|
|
45
46
|
sessionId: process.env.CLEAROTRON_GATHER_SESSION_ID || "",
|
|
47
|
+
sendUntilMs: sendUntilFromEnv(), // this turn's time to stop sending (providers/_shared/send-until.mjs)
|
|
46
48
|
});
|
|
47
49
|
const passthrough = (r) => (r && typeof r === "object" ? (r.text ?? JSON.stringify(r)) : String(r));
|
|
48
50
|
const guard = (fn) => async (args) => {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// session key (tctx.sessionKey) — we thread the run's session key via CLEAROTRON_GATHER_SESSION_KEY so the
|
|
8
8
|
// $0 provider-usage diff still attributes calls to the run.
|
|
9
9
|
import { serve } from "./stdio-server.mjs";
|
|
10
|
+
import { sendUntilFromEnv } from "../../../providers/_shared/send-until.mjs";
|
|
10
11
|
import { crowdNoteFor } from "./crowd-note.mjs"; // how the unit decides on a crowd, beside an answer that carries one
|
|
11
12
|
import { CAPABILITIES, doSearch, doRecordFetch, doImageFetch, doExpandPhoneme, doBatchScreen, doEnumerate, doExecutePlan } from "../../../providers/corsearch/src/core.js";
|
|
12
13
|
import { proposeSupplemental } from "./supplemental.mjs";
|
|
@@ -18,6 +19,7 @@ const tctx = (kind) => ({
|
|
|
18
19
|
agentId: process.env.CLEAROTRON_GATHER_AGENT || "localagent",
|
|
19
20
|
sessionKey: process.env.CLEAROTRON_GATHER_SESSION_KEY || "", // run session key — telemetry attribution
|
|
20
21
|
sessionId: process.env.CLEAROTRON_GATHER_SESSION_ID || "",
|
|
22
|
+
sendUntilMs: sendUntilFromEnv(), // this turn's time to stop sending (providers/_shared/send-until.mjs)
|
|
21
23
|
});
|
|
22
24
|
const passthrough = (r) => (r && typeof r === "object" ? (r.text ?? JSON.stringify(r)) : String(r));
|
|
23
25
|
const guard = (fn) => async (args) => {
|
|
@@ -83,12 +83,11 @@ serve({
|
|
|
83
83
|
description:
|
|
84
84
|
"Record, per position or page, your decision NOT to deliver something that reached your findings surface. " +
|
|
85
85
|
"Every position the digest carried to you, and every page the web notes marked as a candidate or conflict, " +
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
"a defect of this run. One call carries a batch; the answer tells you what was accepted, " +
|
|
86
|
+
"ends as a finding or as a declination with its reason. One left with neither is reported as a defect " +
|
|
87
|
+
"of this run. One call carries a batch; the answer tells you what was accepted, " +
|
|
89
88
|
"what was refused and why, and WHICH ROWS STILL CARRY NO DECISION, so you can finish inside this " +
|
|
90
|
-
"turn. A refusal is about
|
|
91
|
-
"
|
|
89
|
+
"turn. A refusal is about the record's facts, never your legal judgment: a reason is refused only when it " +
|
|
90
|
+
"contradicts the matter's instructions or the record's mark, status, classes or goods as the run holds them.",
|
|
92
91
|
inputSchema: { type: "object", required: ["declinations"], properties: {
|
|
93
92
|
declinations: {
|
|
94
93
|
type: "array",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
// `GET /trademarks/{n}/image` answers 200 with real image bytes. The tool returns metadata + the
|
|
29
29
|
// Vienna figurative-element codes rather than the bytes; see the core for why.
|
|
30
30
|
import { serve } from "./stdio-server.mjs";
|
|
31
|
+
import { sendUntilFromEnv } from "../../../providers/_shared/send-until.mjs";
|
|
31
32
|
import { crowdNoteFor } from "./crowd-note.mjs"; // how the unit decides on a crowd, beside an answer that carries one
|
|
32
33
|
import {
|
|
33
34
|
CAPABILITIES, CRED_ENV, doSearch, doRecordFetch, doBatchScreen, doImageFetch,
|
|
@@ -51,6 +52,7 @@ const tctx = (kind) => ({
|
|
|
51
52
|
agentId: process.env.CLEAROTRON_GATHER_AGENT || "localagent",
|
|
52
53
|
sessionKey: process.env.CLEAROTRON_GATHER_SESSION_KEY || "",
|
|
53
54
|
sessionId: process.env.CLEAROTRON_GATHER_SESSION_ID || "",
|
|
55
|
+
sendUntilMs: sendUntilFromEnv(), // this turn's time to stop sending (providers/_shared/send-until.mjs)
|
|
54
56
|
});
|
|
55
57
|
const passthrough = (r) => (r && typeof r === "object" ? (r.text ?? JSON.stringify(r)) : String(r));
|
|
56
58
|
const guard = (fn) => async (args) => {
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
// Telemetry: the core writes CLEAROTRON_REGISTER_CALL_LOG / CLEAROTRON_REGISTER_RECORD_LOG (the SHARED register
|
|
31
31
|
// ledger) under the provider id `free-tier`, keyed by the gateway session key.
|
|
32
32
|
import { serve } from "./stdio-server.mjs";
|
|
33
|
+
import { sendUntilFromEnv } from "../../../providers/_shared/send-until.mjs";
|
|
33
34
|
import { crowdNoteFor } from "./crowd-note.mjs"; // how the unit decides on a crowd, beside an answer that carries one
|
|
34
35
|
import {
|
|
35
36
|
CAPABILITIES, doSearch, doRecordFetch, doBatchScreen, doImageFetch, doEnumerate, doExecutePlan,
|
|
@@ -47,6 +48,7 @@ const tctx = (kind) => ({
|
|
|
47
48
|
agentId: process.env.CLEAROTRON_GATHER_AGENT || "localagent",
|
|
48
49
|
sessionKey: process.env.CLEAROTRON_GATHER_SESSION_KEY || "",
|
|
49
50
|
sessionId: process.env.CLEAROTRON_GATHER_SESSION_ID || "",
|
|
51
|
+
sendUntilMs: sendUntilFromEnv(), // this turn's time to stop sending (providers/_shared/send-until.mjs)
|
|
50
52
|
});
|
|
51
53
|
const passthrough = (r) => (r && typeof r === "object" ? (r.text ?? JSON.stringify(r)) : String(r));
|
|
52
54
|
const CAPABILITY_OFFICES = CAPABILITIES.offices.covered.join("+");
|
|
@@ -900,6 +900,21 @@ function serverEnv({ sessionKey, agent, runDir, recordAxis, bandGroups = false }
|
|
|
900
900
|
|
|
901
901
|
// Build the mcp-config object for a set of groups. Returns { config: <object>, servers: [...] } or null if
|
|
902
902
|
// the groups need no MCP server (e.g. caselaw-only EUR-Lex via WebFetch needs no server but the bridges do).
|
|
903
|
+
/**
|
|
904
|
+
* The stage's tool config for ONE turn, with that turn's time to stop sending handed to every server
|
|
905
|
+
* (providers/_shared/send-until.mjs). Built per turn, because each attempt has its own deadline. The config
|
|
906
|
+
* is the JSON string the engines take; anything that is not one is returned as it came. PURE.
|
|
907
|
+
*/
|
|
908
|
+
export function withSendUntil(mcpConfigJson, sendUntilMs) {
|
|
909
|
+
if (typeof mcpConfigJson !== "string" || !Number.isFinite(Number(sendUntilMs))) return mcpConfigJson;
|
|
910
|
+
let cfg;
|
|
911
|
+
try { cfg = JSON.parse(mcpConfigJson); } catch { return mcpConfigJson; }
|
|
912
|
+
if (!cfg?.mcpServers || typeof cfg.mcpServers !== "object") return mcpConfigJson;
|
|
913
|
+
for (const server of Object.values(cfg.mcpServers))
|
|
914
|
+
if (server && typeof server === "object") server.env = { ...(server.env ?? {}), CLEAROTRON_GATHER_SEND_UNTIL_MS: String(Math.floor(Number(sendUntilMs))) };
|
|
915
|
+
return JSON.stringify(cfg);
|
|
916
|
+
}
|
|
917
|
+
|
|
903
918
|
export function buildGatherMcpConfig(groups = [], { sessionKey, agent, runDir, recordAxis, bandGroups = false } = {}) {
|
|
904
919
|
const localKeys = new Set(), bridgeKeys = new Set();
|
|
905
920
|
for (const g of groups) { const r = resolveGroup(g); r.local.forEach((k) => localKeys.add(k)); r.bridges.forEach((k) => bridgeKeys.add(k)); }
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
// is the same as one naming them. Owner names are compared as written, never merged. A different scope is a
|
|
16
16
|
// different question, and an earlier answer that failed is no answer: both are asked again. A question that timed out
|
|
17
17
|
// at the gateway is the one failure held, by the executor rather than here: its timeout stands until the envelope
|
|
18
|
-
// close asks it once more (execute-plan.mjs, timedOutBefore).
|
|
18
|
+
// close asks it once more (execute-plan.mjs, timedOutBefore). An answer in a slice the run took out of its band
|
|
19
|
+
// because a check could not prove it ran is asked again too, whatever wrote the band since.
|
|
19
20
|
|
|
20
21
|
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
21
22
|
import { join } from "node:path";
|
|
@@ -23,6 +24,7 @@ import { driverDir } from "../../../shared/driver-dir.mjs";
|
|
|
23
24
|
import { OUTSTANDING_REASON } from "../../../providers/_shared/follow-up-searches.mjs";
|
|
24
25
|
import { awaitsReadingTurn } from "../../../providers/_shared/plan-guards.mjs";
|
|
25
26
|
import { releasedFamilyQids } from "../../withheld-families.mjs";
|
|
27
|
+
import { bandWithSlicesOut, slicesTakenOutByAxis } from "../../../shared/slices-taken-out.mjs";
|
|
26
28
|
|
|
27
29
|
const QUESTION_FIELDS = ["predicate", "term", "terms", "romanizedTerms", "term_literal", "nice_classes", "regions", "goods_text", "owner", "expected_kind"];
|
|
28
30
|
const sorted = (v) => [...v].map((x) => String(x)).sort();
|
|
@@ -88,7 +90,9 @@ export function refuseHeldQuestions(entries, held, planRegions = []) {
|
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
// The run's questions and answers as its files hold them: the frozen plan, each worker's supplemental plan, and every
|
|
91
|
-
// band's blocks by qid
|
|
93
|
+
// band's blocks by qid, with the slices the searches record took out read as the open slices they were delivered as
|
|
94
|
+
// (shared/slices-taken-out.mjs). They are read out after the cache, which knows a band's size and time and not the
|
|
95
|
+
// record's. IMPURE.
|
|
92
96
|
function readRunQuestions(runDir) {
|
|
93
97
|
const units = join(runDir, "register-units");
|
|
94
98
|
const blocksByQid = new Map();
|
|
@@ -96,8 +100,12 @@ function readRunQuestions(runDir) {
|
|
|
96
100
|
try { plans.push({ axis: null, plan: JSON.parse(readFileSync(driverDir(runDir, "register-plan.json"), "utf8")) }); } catch { /* no frozen plan */ }
|
|
97
101
|
let files = [];
|
|
98
102
|
try { files = readdirSync(units); } catch { files = []; }
|
|
103
|
+
const takenOut = slicesTakenOutByAxis(runDir);
|
|
99
104
|
for (const f of files) {
|
|
100
|
-
if (f.endsWith("-band.json"))
|
|
105
|
+
if (f.endsWith("-band.json")) {
|
|
106
|
+
const { blocks } = bandWithSlicesOut(readBlocks(join(units, f)), takenOut.get(f.slice(0, -"-band.json".length)));
|
|
107
|
+
for (const b of blocks) if (b?.qid) blocksByQid.set(String(b.qid), b);
|
|
108
|
+
}
|
|
101
109
|
if (f.endsWith("-supplemental-plan.json")) {
|
|
102
110
|
try { plans.push({ axis: f.slice(0, -"-supplemental-plan.json".length), plan: JSON.parse(readFileSync(join(units, f), "utf8")) }); } catch { /* torn: skipped */ }
|
|
103
111
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// keyed by the GATEWAY session key (tctx.sessionKey) — we thread the run's session key via
|
|
10
10
|
// CLEAROTRON_GATHER_SESSION_KEY so the $0 provider-usage diff still attributes calls to the run.
|
|
11
11
|
import { serve } from "./stdio-server.mjs";
|
|
12
|
+
import { sendUntilFromEnv } from "../../../providers/_shared/send-until.mjs";
|
|
12
13
|
import { crowdNoteFor } from "./crowd-note.mjs"; // how the unit decides on a crowd, beside an answer that carries one
|
|
13
14
|
import { doSearch, doRecordFetch, doEnumerate, doExecutePlan, DEFAULT_BASE } from "../../../providers/signa/src/core.js";
|
|
14
15
|
import { proposeSupplemental } from "./supplemental.mjs";
|
|
@@ -22,6 +23,7 @@ const tctx = (kind) => ({
|
|
|
22
23
|
agentId: process.env.CLEAROTRON_GATHER_AGENT || "localagent",
|
|
23
24
|
sessionKey: process.env.CLEAROTRON_GATHER_SESSION_KEY || "", // run session key — telemetry attribution
|
|
24
25
|
sessionId: process.env.CLEAROTRON_GATHER_SESSION_ID || "",
|
|
26
|
+
sendUntilMs: sendUntilFromEnv(), // this turn's time to stop sending (providers/_shared/send-until.mjs)
|
|
25
27
|
});
|
|
26
28
|
const passthrough = (r) => (r && typeof r === "object" ? (r.text ?? JSON.stringify(r)) : String(r));
|
|
27
29
|
const guard = (fn) => async (args) => {
|
|
@@ -36,6 +36,7 @@ import { PLAN_PREDICATES, PLAN_MAX_OR_WIDTH, PLAN_MAX_NAME_LENGTH, planExactOrWi
|
|
|
36
36
|
import { entryTermIssues } from "../../../providers/_shared/term-shape.mjs";
|
|
37
37
|
import { OUTSTANDING_REASON } from "../../../providers/_shared/follow-up-searches.mjs";
|
|
38
38
|
import { heldAnswers, questionKey } from "./held-answers.mjs";
|
|
39
|
+
import { bandWithSlicesOut, slicesTakenOutByAxis } from "../../../shared/slices-taken-out.mjs";
|
|
39
40
|
import { readWithheldFamilies } from "../../withheld-families.mjs";
|
|
40
41
|
import { addedClassBound, boundAddedClasses } from "../../frame-diff-model.mjs"; // an added class takes one question, the identical mark
|
|
41
42
|
import { scriptQuestionsByMarket } from "../../registration-scripts.mjs"; // one script question per market on the plan's list
|
|
@@ -573,11 +574,13 @@ export async function proposeSupplemental(params, tctx, deps) {
|
|
|
573
574
|
}
|
|
574
575
|
|
|
575
576
|
// A question this worker re-proposes that already holds an answer is not run again: the answer stands. One with no
|
|
576
|
-
// answer, a failed one,
|
|
577
|
+
// answer, a failed one, one whose slice the run took out of the band because a check could not prove it ran
|
|
578
|
+
// (shared/slices-taken-out.mjs), or one this call enriched or gave its markets runs as before.
|
|
577
579
|
let reusedHeld = [];
|
|
578
580
|
try {
|
|
579
581
|
const own = new Map();
|
|
580
|
-
|
|
582
|
+
const { blocks } = bandWithSlicesOut(JSON.parse(readFileSync(outPath, "utf8")), slicesTakenOutByAxis(dirname(dirname(outPath))).get(axis));
|
|
583
|
+
for (const b of blocks) if (b?.qid) own.set(String(b.qid), b);
|
|
581
584
|
reusedHeld = reused.filter((q) => { const b = own.get(String(q)); return b && b.error !== true && !enrichedQids.includes(q) && !staleOnDisk; });
|
|
582
585
|
} catch { reusedHeld = []; }
|
|
583
586
|
const qids = [...minted.map((e) => e.qid), ...reused.filter((q) => !reusedHeld.includes(q))];
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
// visible where a spawn and a reviewer each actually look — the tool list here, the REGISTER_SERVERS
|
|
33
33
|
// filter in gather-config.mjs, and the exact-six pin in driver/test/engine.gather.test.mjs.
|
|
34
34
|
import { serve } from "./stdio-server.mjs";
|
|
35
|
+
import { sendUntilFromEnv } from "../../../providers/_shared/send-until.mjs";
|
|
35
36
|
import { crowdNoteFor } from "./crowd-note.mjs"; // how the unit decides on a crowd, beside an answer that carries one
|
|
36
37
|
import {
|
|
37
38
|
CAPABILITIES, DEFAULT_DB_ENV, doSearch, doRecordFetch, doBatchScreen, doEnumerate, doExecutePlan,
|
|
@@ -48,6 +49,7 @@ const tctx = (kind) => ({
|
|
|
48
49
|
agentId: process.env.CLEAROTRON_GATHER_AGENT || "localagent",
|
|
49
50
|
sessionKey: process.env.CLEAROTRON_GATHER_SESSION_KEY || "", // run session key — telemetry attribution
|
|
50
51
|
sessionId: process.env.CLEAROTRON_GATHER_SESSION_ID || "",
|
|
52
|
+
sendUntilMs: sendUntilFromEnv(), // this turn's time to stop sending (providers/_shared/send-until.mjs)
|
|
51
53
|
});
|
|
52
54
|
const passthrough = (r) => (r && typeof r === "object" ? (r.text ?? JSON.stringify(r)) : String(r));
|
|
53
55
|
const guard = (fn) => async (args) => {
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
import { existsSync, readFileSync, writeFileSync, renameSync } from "node:fs";
|
|
38
38
|
import { isCapabilityGapReason } from "./coverage-ledger.mjs";
|
|
39
39
|
import { fullyDeferredAxes } from "./register-plan.mjs";
|
|
40
|
+
import { readPartNotCompleted } from "./parts-not-completed.mjs";
|
|
40
41
|
|
|
41
42
|
export const SETTLE_SCHEMA_VERSION = 1;
|
|
42
43
|
|
|
@@ -194,9 +195,18 @@ export function settleTriedAfter(prior, dispatched, planVersion) {
|
|
|
194
195
|
return [...out.values()];
|
|
195
196
|
}
|
|
196
197
|
|
|
197
|
-
/**
|
|
198
|
+
/**
|
|
199
|
+
* The run's sticky gaps, by qid → row: the capability gaps it accepted, and the slices a check could not prove ran,
|
|
200
|
+
* which the searches' record took out of the band once their recovery was spent (parts-not-completed.mjs). Neither is
|
|
201
|
+
* sent to the provider again, settled again or re-run to close. Absent or unreadable decision and record ⇒ none.
|
|
202
|
+
*/
|
|
198
203
|
export function readStickyGaps(P) {
|
|
199
|
-
|
|
204
|
+
const out = new Map((readEnvelopeDecision(P)?.sticky_gaps ?? []).map((g) => [String(g?.qid ?? ""), g]).filter(([q]) => q));
|
|
205
|
+
for (const s of (P?.runDir ? readPartNotCompleted(P.runDir, "searches")?.slices : null) ?? []) {
|
|
206
|
+
const qid = String(s?.qid ?? "");
|
|
207
|
+
if (qid && !out.has(qid)) out.set(qid, { qid, axis: String(s.axis ?? ""), reason: String(s.reason ?? "").slice(0, 400) });
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
200
210
|
}
|
|
201
211
|
|
|
202
212
|
/** One history entry per superseded decision. Returns the prior history with the old decision appended. */
|
|
@@ -225,7 +235,7 @@ export function supersede(prior, source) {
|
|
|
225
235
|
export async function settleReceipt({ P, plan, receipt, dispatch = null, rejoin = null, now = null }) {
|
|
226
236
|
const deferred = receipt?.deferred ?? [];
|
|
227
237
|
const prior = readEnvelopeDecision(P);
|
|
228
|
-
const sticky = new Set((
|
|
238
|
+
const sticky = new Set(readStickyGaps(P).keys());
|
|
229
239
|
let { accepted, suspect } = partitionReceiptDeferrals(plan, receipt, { sticky });
|
|
230
240
|
const closed = [], closeFailed = [];
|
|
231
241
|
const tried = new Map((prior?.settle_tried ?? []).map((t) => [String(t?.qid ?? ""), t]));
|